决策树模型算法研究与案例分析

(白宁超 2018年8月30日11:46:14)

导读:决策树算法是一种基本的分类与回归方法,是最经常使用的算法之一。决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是基于规则的集合。本文首先介绍决策树定义、工作原理、算法流程、优缺点等,然后结合案例进行分析。(本文原创,转载必须注明出处)

理论介绍

什么是决策树

  • 维基百科:决策树(Decision Tree)是一个预测模型;他代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。数据挖掘中决策树是一种经常要用到的技术,可以用于分析数据,同样也可以用来作预测。从数据产生决策树的机器学习技术叫做决策树学习,通俗说就是决策树。
  • 分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。

用决策树对需要测试的实例进行分类:从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分配到叶结点的类中。

什么是信息熵和信息增益

  • 熵(entropy): 熵指的是体系的混乱的程度,在不同的学科中也有引申出的更为具体的定义,是各领域十分重要的参量。
  • 信息论(information theory)中的熵(香农熵): 是一种信息的度量方式,表示信息的混乱程度,也就是说:信息越有序,信息熵越低。例如:火柴有序放在火柴盒里,熵值很低,相反,熵值很高。
  • 信息增益(information gain): 在划分数据集前后信息发生的变化称为信息增益,信息增益越大,确定性越强。

决策树工作原理

'''
决策树工作原理:基于迭代的思想。
'''
def createBranch():
    检测数据集中的所有数据的分类标签是否相同:
        If so return 类标签
        Else:
            寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
            划分数据集
            创建分支节点
                for 每个划分的子集
                    调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
            return 分支节点

决策树算法流程

收集数据:可以使用任何方法。
准备数据:树构造算法 (这里使用的是ID3算法,只适用于标称型数据,这就是为什么数值型数据必须离散化。 还有其他的树构造算法,比如CART)
分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
训练算法:构造树的数据结构。
测试算法:使用训练好的树计算错误率。
使用算法:此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。

决策树优缺点

相对于其他数据挖掘算法,决策树在以下几个方面拥有优势:

1 决策树易于理解和实现.人们在通过解释后都有能力去理解决策树所表达的意义。
2 对于决策树,数据的准备往往是简单或者是不必要的.其他的技术往往要求先把数据一般化,比如去掉多余的或者空白的属性。
3 能够同时处理数据型和常规型属性。其他的技术往往要求数据属性的单一。
4 是一个白盒模型如果给定一个观察的模型,那么根据所产生的决策树很容易推出相应的逻辑表达式。
5 易于通过静态测试来对模型进行评测。表示有可能测量该模型的可信度。
6 在相对短的时间内能够对大型数据源做出可行且效果良好的结果。
7 计算复杂度不高,输出结果易于理解,数据有缺失也能跑,可以处理不相关特征。

缺点:

1 容易过拟合。
2 对于那些各类别样本数量不一致的数据,在决策树当中信息增益的结果偏向于那些具有更多数值的特征。

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

1 数值型:数值型目标变量则可以从无限的数值集合中取值,如0.100,42.001等 (数值型目标变量主要用于回归分析)
2 标称型:标称型目标变量的结果只在有限目标集中取值,如真与假(标称型目标变量主要用于分类)

案例描述:加深决策树理解

案例描述

小王是一家著名高尔夫俱乐部的经理。但是他被雇员数量问题搞得心情十分不好。某些天好像所有人都来玩高尔夫,以至于所有员工都忙的团团转还是应付不过来,而有些天不知道什么原因却一个人也不来,俱乐部为雇员数量浪费了不少资金。小王的目的是通过下周天气预报寻找什么时候人们会打高尔夫,以适时调整雇员数量。因此首先他必须了解人们决定是否打球的原因。

数据采集

在2周时间内我们得到以下记录:

天气状况有晴,云和雨;气温用华氏温度表示;相对湿度用百分比;还有有无风。当然还有顾客是不是在这些日子光顾俱乐部。最终他得到了14行5列的数据表格。

构建决策树

决策树模型就被建起来用于解决问题。

结果分析

决策树是一个有向无环图。根结点代表所有数据。分类树算法可以通过变量outlook,找出最好地解释非独立变量play(打高尔夫的人)的方法。变量outlook的范畴被划分为以下三个组:晴天,多云天和雨天。

我们得出第一个结论:如果天气是多云,人们总是选择玩高尔夫,而只有少数很着迷的甚至在雨天也会玩。

接下来我们把晴天组的分为两部分,我们发现顾客不喜欢湿度高于70%的天气。最终我们还发现,如果雨天还有风的话,就不会有人打了。

这就通过分类树给出了一个解决方案。小王(老板)在晴天,潮湿的天气或者刮风的雨天解雇了大部分员工,因为这种天气不会有人打高尔夫。而其他的天气会有很多人打高尔夫,因此可以雇用一些临时员工来工作。

决策树算法实现与分析

案例: 判定鱼类和非鱼类

案例需求描述

我们采集海洋生物数据信息,选择其中5条如下表所示,从诸多特征中选择2个最主要特征,以及判定是否属于鱼类(此处我们选择二分类法即只考虑鱼类和非鱼类)。
根据这些信息如何创建一个决策树进行分类并可视化展示?

收集数据

部分数据采集信息

序号 不浮出水面是否可以生存 是否有脚蹼 属于鱼类
1
2
3
4
5

我们将自然语言数据转化为计算机输入数据,代码实现如下:

'''创建数据集,返回数据集和标签'''
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

运行查看数据集的特征向量和分类标签:

# 1 打印数据集和标签
dataset,label=createDataSet()
print(dataset)
print(label)

运行结果:

[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
['no surfacing', 'flippers']

准备数据

由于我们输入的数据已经是数据预处理后的数据,这一步不需要进行。

分析数据

我们得到数据之后,到底是按照第一个特征即(不浮出水面是否可以生存)还是第二个特征即(是否有脚蹼)进行数据划分呢?这里面就需要找到一种量化的方法判断特征的选择。在介绍具体数据划分方法之前,我们首先明白划分数据集的最大原则是:将无序的数据变得更加有序

1948 年,香农引入信息熵,将其定义为离散随机事件的出现概率。一个系统越有序,信息熵就越低;反之,一个系统越混乱,信息熵就越高。所以说,信息熵可以被认为是系统有序化程度的一个度量。

这里就要用的信息熵的概念,熵越高表示混合数据越多,度量数据集无序程度。我们看下信息熵的数学描述(具体请自行查找熵相关知识):

计算数据集的香农熵(信息期望值)

根据公式比较容易理解的实现方法1如下:

'''计算数据集的香农熵(信息期望值):熵越高表示混合数据越多,度量数据集无序程度'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet) # 计算数据集中实例总数
    labelCounts = {} # 创建字典,计算分类标签label出现的次数
    for featVec in dataSet:
        currentLabel = featVec[-1] # 记录当前实例的标签
        if currentLabel not in labelCounts.keys():# 为所有可能的分类创建字典
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
        # print(featVec, labelCounts) # 打印特征向量和字典的键值对

    # 对于label标签的占比,求出label标签的香农熵
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries # 计算类别出现的概率。
        shannonEnt -= prob * log(prob, 2) # 计算香农熵,以 2 为底求对数
    print(Decimal(shannonEnt).quantize(Decimal('0.00000')))
    return shannonEnt

更高级的实现方法2如下:

'''计算数据集的香农熵(信息期望值):熵越高表示混合数据越多,度量数据集无序程度'''
def calcShannonEnt(dataSet):
    # 需要对 list 中的大量计数时,可以直接使用Counter,不用新建字典来计数
    label_count = Counter(data[-1] for data in dataSet) # 统计标签出现的次数
    probs = [p[1] / len(dataSet) for p in label_count.items()] # 计算概率
    shannonEnt = sum([-p * log(p, 2) for p in probs]) # 计算香农熵
    print(Decimal(shannonEnt).quantize(Decimal('0.00000')))
    return shannonEnt

调用运行如下:

# 2 计算数据集的熵
calcShannonEnt(dataset)

按照给定的特征划分数据集

我们根据信息熵度量出来的特征,进行数据集划分方法1如下:

'''划分数据集:按照特征划分'''
def splitDataSet(dataSet, index, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[index] == value:# 判断index列的值是否为value
            reducedFeatVec = featVec[:index] # [:index]表示取前index个特征
            reducedFeatVec.extend(featVec[index+1:]) # 取接下来的数据
            retDataSet.append(reducedFeatVec)
    print(retDataSet)
    return retDataSet

我们根据信息熵度量出来的特征,进行数据集划分方法2如下:

'''划分数据集:按照特征划分'''
def splitDataSet(dataSet, index, value):
    retDataSet = [data for data in dataSet for i, v in enumerate(data) if i == index and v == value]
    print(retDataSet)
    return retDataSet

指定特征的数据集划分方法调用

#3 划分数据集
splitDataSet(dataset,0,1)

运行结果如下:

[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no']]选择最好的数据集划分方式

选择最好的数据集划分方式:特征选择,划分数据集、计算最好的划分数据集特征,方法1如下:

'''
注意:一是数据集列表元素具备相同数据长度,二是最后一列是标签列
'''
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 # 特征总个数, 最后一列是标签
    baseEntropy = calcShannonEnt(dataSet) # 计算数据集的信息熵
    bestInfoGain, bestFeature = 0.0, -1 # 最优的信息增益值, 和最优的Featurn编号
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet] # 获取各实例第i+1个特征
        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
        print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    # print(bestFeature)
    return bestFeature

选择最好的数据集划分方式:特征选择,划分数据集、计算最好的划分数据集特征,方法2如下:

'''
注意:一是数据集列表元素具备相同数据长度,二是最后一列是标签列
'''
def chooseBestFeatureToSplit(dataSet):
    base_entropy = calcShannonEnt(dataSet) # 计算初始香农熵
    best_info_gain = 0
    best_feature = -1
    # 遍历每一个特征
    for i in range(len(dataSet[0]) - 1):
        # 对当前特征进行统计
        feature_count = Counter([data[i] for data in dataSet])
        # 计算分割后的香农熵
        new_entropy = sum(feature[1] / float(len(dataSet)) * calcShannonEnt(splitDataSet(dataSet, i, feature[0])) for feature in feature_count.items())
        # 更新值
        info_gain = base_entropy - new_entropy
        # print('No. {0} feature info gain is {1:.3f}'.format(i, info_gain))
        if info_gain > best_info_gain:
            best_info_gain = info_gain
            best_feature = i
    # print(best_feature)
    return best_feature

选择最好的数据集划分方法调用

# 4 选择最好的数据集划分方式
chooseBestFeatureToSplit(dataset))

运行结果如下:

infoGain= 0.4199730940219749 bestFeature= 0 0.9709505944546686 0.5509775004326937
infoGain= 0.17095059445466854 bestFeature= 1 0.9709505944546686 0.8
选择:0

训练算法:构造树的数据结构

创建树的函数代码如下:

'''创建决策树'''
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
    # 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。
    # count() 函数是统计括号中的值在list中出现的次数
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    # 选择最优的列,得到最优列对应的label含义
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 获取label的名称
    bestFeatLabel = labels[bestFeat]
    # 初始化myTree
    myTree = {bestFeatLabel: {}}
    # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    # del(labels[bestFeat])
    # 取出最优列,然后它的branch做分类
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 求出剩余的标签label
        subLabels = labels[:]
        # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
        # print('myTree', value, myTree)
    print(myTree)
    return myTree

其中多数表决方法决定叶子节点的分类实现如下:

'''多数表决方法决定叶子节点的分类:选择出现次数最多的一个结果'''
def majorityCnt(classList):
    # -----------多数表决实现的方式一--------------
    # classCount = {}   # 标签字典,用于统计类别频率
    # for vote in classList: # classList标签的列表集合
    #     if vote not in classCount.keys():
    #         classCount[vote] = 0
    #     classCount[vote] += 1
    # # 取出结果(yes/no),即出现次数最多的结果
    # sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    # print('sortedClassCount:', sortedClassCount)
    # return sortedClassCount[0][0]


    # -----------多数表决实现的方式二-----------------
    major_label = Counter(classList).most_common(1)[0]
    print('sortedClassCount:', major_label[0])
    return major_label[0]

调用方法:

# 6创建决策树
createTree(dataset, label)

运行结果:

{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

结果分析:
此时,每次生成决策树数据都需要大量的计算,并且耗时,最好是每次直接调用生成结果。这里就需要使用Python模块pickle序列化对象,其存储决策树读取决策树代码实现如下:

'''使用pickle模块存储决策树'''
def storeTree(inputTree, filename):
    import pickle
    # -------------- 第一种方法 --------------
    fw = open(filename, 'wb')
    pickle.dump(inputTree, fw)
    fw.close()

    # -------------- 第二种方法 --------------
    with open(filename, 'wb') as fw:
        pickle.dump(inputTree, fw)


def grabTree(filename):
    import pickle
    fr = open(filename,'rb')
    return pickle.load(fr)

测试算法:使用决策树执行分类

用决策树进行鱼类属于分类实现如下:

'''用决策树分类函数'''
def classify(inputTree, featLabels, testVec):
    firstStr = list(inputTree.keys())[0] # 获取tree的根节点对于的key值
    secondDict = inputTree[firstStr]  # 通过key得到根节点对应的value
    # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    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]
    print(classLabel)
    return classLabel

调用方法:

# 7 用决策树分类函数
myTree = treePlotter.retrieveTree(0)
# print(myTree)
classify(myTree,label,[1,0])

运行结果:

分类结果:no surfacing

决策树分类器实现

使用算法此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。

'''决策树判断是否是鱼'''
def fishTest():
    # 1.创建数据和结果标签
    myDat, labels = createDataSet()

    # 计算label分类标签的香农熵
    calcShannonEnt(myDat)

    # 求第0列 为 1/0的列的数据集【排除第0列】
    print('1---', splitDataSet(myDat, 0, 1))
    print('0---', splitDataSet(myDat, 0, 0))

    # 计算最好的信息增益的列
    print(chooseBestFeatureToSplit(myDat))

    import copy
    myTree = createTree(myDat, copy.deepcopy(labels))
    print(myTree)
    # [1, 1]表示要取的分支上的节点位置,对应的结果值
    print(classify(myTree, labels, [1, 1]))

    # 画图可视化展现
    treePlotter.createPlot(myTree)

调用决策树分类方法:

# 9 决策树判断是否是鱼
fishTest()

运行结果如下:

1--- [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no']]
0--- [[0, 1, 'no'], [0, 1, 'no']]
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
yes

可视化结果

决策树实际应用:预测隐形眼镜的测试代码

项目概述

隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。

开发流程

收集数据: 提供的文本文件。
解析数据: 解析 tab 键分隔的数据行
分析数据: 快速检查数据,确保正确地解析数据内容,使用 createPlot() 函数绘制最终的树形图。
训练算法: 使用 createTree() 函数。
测试算法: 编写测试函数验证决策树可以正确分类给定的数据实例。
使用算法: 存储树的数据结构,以便下次使用时无需重新构造树。
收集数据:提供的文本文件

数据读取

文本文件数据格式如下:

young    myope    no    reduced    no 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    myope    no    reduced    no lenses
presbyopic    myope    no    normal    no lenses
presbyopic    myope    yes    reduced    no lenses
presbyopic    myope    yes    normal    hard
presbyopic    hyper    no    reduced    no lenses
presbyopic    hyper    no    normal    soft
presbyopic    hyper    yes    reduced    no lenses
presbyopic    hyper    yes    normal    no lenses

代码实现: 编写测试函数验证决策树可以正确分类给定的数据实例。

'''预测隐形眼镜的测试代码'''
def ContactLensesTest():
    # 加载隐形眼镜相关的 文本文件 数据
    fr = open('lenses.txt')
    # 解析数据,获得 features 数据
    lenses = [inst.strip().split('    ') for inst in fr.readlines()]
    # 得到数据的对应的 Labels
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    # 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
    lensesTree = createTree(lenses, lensesLabels)
    print(lensesTree)
    # 画图可视化展现
    treePlotter.createPlot(lensesTree)

运行结果

调用方法

# 10 预测隐形眼镜类型
ContactLensesTest()

运行结果

{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'no': {'age': {'young': 'soft', 'pre': 'soft', 'presbyopic': {'prescript': {'myope': 'no lenses', 'hyper': 'soft'}}}}, 'yes': {'prescript': {'myope': 'hard', 'hyper': {'age': {'young': 'hard', 'pre': 'no lenses', 'presbyopic': 'no lenses'}}}}}}}}

决策树可视化

完整代码下载

源码请进QQ群436303759文件下载:

作者声明

本文版权归作者所有,旨在技术交流使用。未经作者同意禁止转载,转载后需在文章页面明显位置给出原文连接,否则相关责任自行承担。白宁超的官网 https://bainingchao.github.io/


http://credit-n.ru/zaymyi-next.html

作者 白 宁超

白宁超,工学硕士,现工作于四川省计算机研究院,研究方向是自然语言处理和机器学习。曾参与国家自然基金项目和四川省科技支撑计划等多个省级项目。著有《自然语言处理理论与实战》一书。

在 “决策树模型算法研究与案例分析” 有 1 条评论
  1. 有个问题:这个函数createTree()里面,
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
    这两行代码,根据例子里面的数据,labels只有两个,如果bestFeat是第 5 或者第6列,bestFeatLabel 就取不到了,代码会报错?

    [回复]

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注