决策树

一、介绍

       决策树算法可以读取数据集合,构建决策树。决策树很多任务都是为了数据中所蕴涵的信息,因此决策树可以使一个不熟悉的数据集合,并从中提取出一系列规则。机器学习算法将使用这些规则。

                                                          

例如这个例子,邮件分类的决策树。长方形表示判断,椭圆形代表终止。首先判断邮件的地址,如果是,则化为无聊时需要阅读的邮件,如果否则需要继续判断邮件中单词中是否包含曲棍球,同样如果是则化为需要及时处理的朋友邮件,如果否,则判为无需阅读的垃圾邮件。这是一个简单的决策树。接下来,我们将进行决策树的构造。

二、决策树的构造

       在构造决策树时,我们需要有判断条件,那么将哪个特征在划分数据分类时起决定性作用。这是我们的首要问题,找到决定性特征,划分出最好的结果,就要对每个特征进行评估。原始数据集会被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则不需要在进行划分,如果数据不属于同一类型,则需要进行重复划分数据集的过程。

基本的思想:检测数据集中每个子项是否属于同一分类。如果属于,则返回类标签。如果不属于,继续寻找划分数据集的最好特征,划分数据集,创建分支节点,然后对每个划分的子集,继续检测是否属于同一分类,循环上述过程。最后返回分支节点。

1.信息增益

划分数据集的大原则是:将无序的数据集变得更加有序,组织杂乱无章数据的一种方法是使用信息论度量信息。信息论是量化处理信息的分支科学,我们可以在划分数据之前使用信息论量化度量信息的内容。在划分数据之前之后信息发生的变化成为信息增益。获得信息增益最高的特征就是最好的选择。那么就要考虑如何计算信息增益,集合信息的度量方式称为香农熵或者简称为熵。熵为信息的期望值。如果待分类的事物可能划分在多个分类之中,则符号x_{i}的信息定义为:l(x_{i})=-log_{2}p(x_{i})其中p(x_{i})是选择该分类的概率。熵:H=-\sum_{i=1}^{n}p(x_{i})log_{2}p(x_{i})

from math import log

def calcShannonEnt(dataSet):
    """
    计算香浓熵
    1.统计每一个类别的个数
    2.计算每一个类别的概率,每一个类别的个数除总个数
    3.利用香浓熵公式求出香浓熵
    """
    numTotal = len(dataSet)   # 数据集的总数
    labelCounts = {}
    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]) / numTotal  # 计算概率
        shannonEnt -= prob * log(prob, 2)    # 计算香浓熵
    return shannonEnt

得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集,接下来我们将学习如何划分数据集以及如何度量信息增益。在熵的计算中,混合的数据越多,也就是种类越多,熵就越大。

2.划分数据集

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

def splitData(dataSet, axis, value):
    """
    按照给定特征划分数据集
    :param dataSet: 待划分的数据集
    :param axis:划分数据集的特征
    :param value:特征的返回值
    :return:抽取出来的数据集
    """
    retDataSet = []  # 创建一个新的列表
    for featVec in dataSet:
        if featVec[axis] == value:
            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 = splitData(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. python不需考虑内存分配问题,在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生命周期,因此在划分数据集的操作中,需要在函数的开始声明一个新列表对象。为了保证不修改原数据。
  2. 信息增益是熵的减少,或者是数据无序度的减少。

经过选择之后,我们可以找出最好的特征用于划分数据。

3.递归构建决策树

我们已经完成了从数据集构造决策树算法所需要的子功能模块。工作原理:得到原始数据集,然后基于最好的属性值划分数据集,因为可能有多个特征值,因此可能存在大于两个分支的数据集划分。第一次划分后,数据将被向下传递到树分支的下一个节点,然后再这个节点,我们可以再次划分数据。因此采用递归的原则处理数据集。递归的结束条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都是有相同的分类。

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]

def createTree(dataSet, labels):
    """
    创建决策树
    :param dataSet: 数据集
    :param labels: 标签列表
    :return: 决策树
    """
    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(splitData(dataSet, bestFeat, value), subLabels)  # 递归创建树
    return myTree

三、使用matplotlib注解绘制树形图

1.matplotlb注解

maplotlib提供了一个注解工具annotations,可以在数据图形上添加文本注释。注解通常用于解释数据的内容。由于数据上面直接存在文本描述非常不好看,因此工具内嵌支持带箭头的划线工具,使得我们可以在合适的地方指向数据位置,并在此处添加描述信息。

首先,我们使用文本绘制树节点:

import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")

def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    """ 绘制带箭头的注解 """
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords = 'axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va='center', ha='center',bbox=nodeType, arrowprops=arrow_args)

 

2.构造注解树

绘制一颗完整的树,我们必须知道有多少个叶节点,以便可以正确确定x轴的长度,还需要知道树有多少层,以便可以确定y轴的高度。我们使用如下函数来获取叶子节点的数目和树的层数。下面的代码包含一个完整的构造画决策树的过程。

def getNumLeafs(myTree):
    """ 获取叶子节点的数目,以此计算图的宽度 """
    numLeafs = 0        # 叶子节点的数目
    firstStr = list(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 = list(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 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 plotMidText(cntrPt, parentPt, txtString):
    # 在父子节点中添加文本信息
    xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0]
    yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString)

def plotTree(myTree, parentPt, nodeText):
    # 计算宽和高
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    firstStr = list(myTree.keys())[0]
    cntrPt = (plotTree.xOff + (1.0 +float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff)
    # 标记子节点属性值
    plotMidText(cntrPt, parentPt, nodeText)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    # 减少y偏移
    plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD
    for key in secondDict.keys():
        if type(secondDict[key]).__name__ == 'dict':
            plotTree(secondDict[key], cntrPt, str(key))
        else:
            plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0 / plotTree.totalD

def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    plotTree.xOff = -0.5 / plotTree.totalW
    plotTree.yOff = 1.0
    plotTree(inTree, (0.5, 1.0), '')
    plt.show()

四、测试和存储分类器

接下来,我们将利用决策树构建分类器,以及实际应用中如何存储分类器。构造了决策树之后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型

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

现在我们创建了使用决策树的分类器,但是每次使用都必须重新构造分类器,因此,可以将分类器存储在硬盘上,使用构建好的决策树进行分类可以节省很多时间。我们使用pickle模块序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。

def storeTree(inputTree, filename):
    """ 存储决策树 """
    fw = open(filename, 'wb')
    pickle.dump(inputTree, fw)
    fw.close()

def grabTree(filename):
    """ 加载决策树 """
    fr = open(filename, 'rb')
    return pickle.load(fr)

五、使用决策树预测隐形眼镜类型

下面我们使用隐形眼镜数据集测试决策树算法,

# 读取数据
fr = open('lenses.txt')
# 对数据做处理
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 标签数据
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
# 创建决策树
lensesTree = createTree(lenses, lensesLabels)
storeTree(lensesTree, 'lensesTree.txt')
createPlot(lensesTree)

 

到此,我们完成了决策树算法。然而这个很好的匹配了数据,但是匹配的选项有可能过多,造成过度匹配。我们可以裁剪决策树解决这个问题,我们使用的决策树算法为ID3.这个算法虽然很好,但是也存在缺陷。决策树还包括C4.5,CART等算法。

全部代码链接:https://github.com/guoyuantao/MachineLearning/tree/master/Decision_Tree

注意:此文章的内容参考《机器学习实战》。博客的主要目的为记录个人所学的知识。

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值