机器学习之决策树

决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。

下面介绍分类决策树,分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部节点和叶节点,内部节点表示一个特征或属性,叶节点表示一个类。 

分类的时候,从根节点开始,对实例的某一个特征进行测试,根据测试结果,将实例分配到其子结点;此时,每一个子结点对应着该特征的一个取值。如此递归向下移动,直至达到叶结点,最后将实例分配到叶结点的类中。 

下图是决策树的例子


决策树的构建

决策树的构建有三个步骤:特征选择、决策树生成、决策树的修建

特征选择

为什么要进行特征选择呢?很简单,不同的特征会对分类结果造成不同的影响,有的特征甚至无法对分类造成影响,比如说皮肤黑白并不能作为区分男女的特征,所以说我们需要选择更好的特征来生成决策树。但是特征选择显然是不能靠人工来判断的。在机器学习中,我们通过信息增益来作为特征选择的标准。


假设我们要针对上面的数据实现是否批准贷款申请的决策树,如果我们根据年龄和是否有工作可以得出以下两种可能的决策树


那么那个更好呢?这个时候我们计算出每个特征值划分数据集是所获得的信息增益,增益最高的特征就是最好的。

信息增益

信息熵:信息的基本作用就是消除人们对事物的不确定性。多数粒子组合之后,在它似像非像的形态上押上有价值的数码,具体地说,这就是一个在博弈对局中现象信息的混乱。

信息熵

香农指出,它的准确信息量应该是
-(p1*log(2,p1) + p2 * log(2,p2) + ... +p32 *log(2,p32)),

其中,p1,p2 , ...,p32 分别是这 32 个球队夺冠的概率。香农把它称为“信息熵” (Entropy),一般用符号 H 表示,单位是比特。

变量的不确定性越大,熵也就越大,把它搞清楚所需要的信息量也就越大。也就是说价值越大

香农熵公式如下


P(x):选择该分类的概率(银行贷款数据中只有两个分类,yes和no)

经验熵

如果说熵中的概率是数据估计得出来的时候,计算出的熵叫做经验熵,也可以理解为根据自己所有的数据来计算概率。

接下来是用Python代码实现上述公式

def creatDataset():
    """
    创建数据集,用于练习香农公式
    :return: dataSet:数据集,labels:分类属性
    """
    dataSet = [
        [0, 0, 0, 0, 'no'],
        [0, 0, 0, 1, 'no'],
        [0, 1, 0, 1, 'yes'],
        [0, 1, 1, 0, 'yes'],
        [0, 0, 0, 0, 'no'],
        [1, 0, 0, 0, 'no'],
        [1, 0, 0, 1, 'no'],
        [1, 1, 1, 1, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [2, 0, 1, 2, 'yes'],
        [2, 0, 1, 1, 'yes'],
        [2, 1, 0, 1, 'yes'],
        [2, 1, 0, 2, 'yes'],
        [2, 0, 0, 0, 'no']
    ]
    """
    年龄:0:青年、1:中年、2:老年
    工作:0:否、1:是
    房子:0:否、1:是
    信贷情况:0:一般好、1:好、2:非常好
    类别:no:否、yes:是
    """
    labels = ['年龄','工作','房子','信贷情况']
    return dataSet,labels

def calcShannonEntropy(dataSet):
    """
    计算香农熵
    :return: ShannonEntropy:香农熵
    """
    numEntires = len(dataSet)
    labelCounts = {}#用字典保存每个标签出现的次数
    for featVec in dataSet:#遍历每组特征向量
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    print(labelCounts)
    shannonEntroy = 0.0#香农熵
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntires
        shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
    return shannonEntroy

运行结果


由上面的结果可以得出经验熵

条件熵

条件熵是什么意思呢?H(Y|X)是指:一直随机变量X的条件下随机变量Y的不确定性,公式如下


了解了上述的概念后我们可以讲解信息增益了:

信息增益:熵 - 条件熵

下面是知乎上某位打算对于信息增益的理解:通俗地讲,X(明天下雨)是一个随机变量,X的熵可以算出来, Y(明天阴天)也是随机变量,在阴天情况下下雨的信息熵我们如果也知道的话(此处需要知道其联合概率分布或是通过数据估计)即是条件熵。两者相减就是信息增益!原来明天下雨例如信息熵是2,条件熵是0.01(因为如果是阴天就下雨的概率很大,信息就少了),这样相减后为1.99,在获得阴天这个信息后,下雨信息不确定性减少了1.99!是很多的!所以信息增益大!也就是说,阴天这个信息对下雨来说是很重要的!所以在特征选择的时候常常用信息增益,如果IG(信息增益大)的话那么这个特征对于分类来说很关键~~ 决策树就是这样来找特征的!

下面是计算信息争议的Python代码

from math import log

def creatDataset():
    """
    创建数据集,用于练习香农公式
    :return: dataSet:数据集,labels:分类属性
    """
    dataSet = [
        [0, 0, 0, 0, 'no'],
        [0, 0, 0, 1, 'no'],
        [0, 1, 0, 1, 'yes'],
        [0, 1, 1, 0, 'yes'],
        [0, 0, 0, 0, 'no'],
        [1, 0, 0, 0, 'no'],
        [1, 0, 0, 1, 'no'],
        [1, 1, 1, 1, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [2, 0, 1, 2, 'yes'],
        [2, 0, 1, 1, 'yes'],
        [2, 1, 0, 1, 'yes'],
        [2, 1, 0, 2, 'yes'],
        [2, 0, 0, 0, 'no']
    ]
    """
    年龄:0:青年、1:中年、2:老年
    工作:0:否、1:是
    房子:0:否、1:是
    信贷情况:0:一般好、1:好、2:非常好
    类别:no:否、yes:是
    """
    labels = ['年龄','工作','房子','信贷情况']
    return dataSet,labels

def calcShannonEntropy(dataSet):
    """
    计算香农熵
    :return: ShannonEntropy:香农熵
    """
    numEntires = len(dataSet)
    labelCounts = {}#用字典保存每个标签出现的次数
    for featVec in dataSet:#遍历每组特征向量
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEntroy = 0.0#香农熵
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntires
        shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
    return shannonEntroy

def splitDataSet(dataSet, axis, value):
    retDataSet = []#创建返回的数据集列表
    for featVec in dataSet:#遍历数据集
        #print("1",featVec)
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]#去掉axis特征
            #print("2",reducedFeatVec)
            reducedFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
            #print("3",reducedFeatVec)
            retDataSet.append(reducedFeatVec)
            #print("4", retDataSet)

    return retDataSet#返回划分后的数据集

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1#特征数量
    baseEntropy = calcShannonEntropy(dataSet)#计算数据集的香农熵
    bestInfoGain = 0.0#信息增益
    bestFeature = -1#最优特征的索引值
    for i in range(numFeatures):#遍历所有特征
        #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #print("featList:",featList)
        uniqueVals = set(featList)#创建set集合{},元素不可重复
        #print(uniqueVals)
        newEntropy = 0.0#经验条件熵
        for value in uniqueVals:#计算信息增益
            #print("i:",i,",value:",value)
            subDataSet = splitDataSet(dataSet, i, value)#subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet))#计算子集的概率
            newEntropy += prob * calcShannonEntropy(subDataSet)#根据公式计算经验条件熵
        infoGain = baseEntropy - newEntropy#信息增益
        print("第%d个特征的增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
        if (infoGain > bestInfoGain):#计算信息增益
            bestInfoGain = infoGain#更新信息增益,找到最大的信息增益
            bestFeature = i#记录信息增益最大的特征的索引值
    return bestFeature#返回信息增益最大的特征的索引值
if __name__ == '__main__':
    dataSet,labels = creatDataset()
    chooseBestFeatureToSplit(dataSet)

决策树的生成和裁剪

决策树的生成算法有ID3、C4.5、CART

ID3算法

ID3算法最早是由罗斯昆(J. Ross Quinlan)于1975年在悉尼大学提出的一种分类预测算法,算法的核心是“信息熵”。ID3算法通过计算每个属性的信息增益,认为信息增益高的是好属性,每次划分选取信息增益最高的属性为划分标准,重复这个过程,直至生成一个能完美分类训练样例的决策树。
决策树是对数据进行分类,以此达到预测的目的。该决策树方法先根据训练集数据形成决策树,如果该树不能对所有对象给出正确的分类,那么选择一些例外加入到训练集数据中,重复该过程一直到形成正确的决策集。决策树代表着决策集的树形结构。

决策树由决策结点、分支和叶子组成。决策树中最上面的结点为根结点,每个分支是一个新的决策结点,或者是树的叶子。每个决策结点代表一个问题或决策,通常对应于待分类对象的属性。每一个叶子结点代表一种可能的分类结果。沿决策树从上到下遍历的过程中,在每个结点都会遇到一个测试,对每个结点上问题的不同的测试输出导致不同的分支,最后会到达一个叶子结点,这个过程就是利用决策树进行分类的过程,利用若干个变量来判断所属的类别。(来着百度百科)

ID3算法Python实现

from math import log
import operator

def creatDataset():
    """
    创建数据集,用于练习香农公式
    :return: dataSet:数据集,labels:分类属性
    """
    dataSet = [
        [0, 0, 0, 0, 'no'],
        [0, 0, 0, 1, 'no'],
        [0, 1, 0, 1, 'yes'],
        [0, 1, 1, 0, 'yes'],
        [0, 0, 0, 0, 'no'],
        [1, 0, 0, 0, 'no'],
        [1, 0, 0, 1, 'no'],
        [1, 1, 1, 1, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [1, 0, 1, 2, 'yes'],
        [2, 0, 1, 2, 'yes'],
        [2, 0, 1, 1, 'yes'],
        [2, 1, 0, 1, 'yes'],
        [2, 1, 0, 2, 'yes'],
        [2, 0, 0, 0, 'no']
    ]
    """
    年龄:0:青年、1:中年、2:老年
    工作:0:否、1:是
    房子:0:否、1:是
    信贷情况:0:一般好、1:好、2:非常好
    类别:no:否、yes:是
    """
    labels = ['年龄','工作','房子','信贷情况']
    return dataSet,labels

def calcShannonEntropy(dataSet):
    """
    计算香农熵
    :return: ShannonEntropy:香农熵
    """
    numEntires = len(dataSet)
    labelCounts = {}#用字典保存每个标签出现的次数
    for featVec in dataSet:#遍历每组特征向量
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEntroy = 0.0#香农熵
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntires
        shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
    return shannonEntroy

def splitDataSet(dataSet, axis, value):
    """
    为计算条件信息熵,划分数据集
    :param dataSet: 要划分的数据集
    :param axis: 根据第几个特征划分
    :param value: 特征值
    :return:划分完的数据集
    """
    retDataSet = []#创建返回的数据集列表
    for featVec in dataSet:#遍历数据集
        #print("1",featVec)
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]#去掉axis特征
            #print("2",reducedFeatVec)
            reducedFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
            #print("3",reducedFeatVec)
            retDataSet.append(reducedFeatVec)
            #print("4", retDataSet)

    return retDataSet#返回划分后的数据集

def chooseBestFeatureToSplit(dataSet):
    """
    选择最好的特征划分
    :param dataSet: 
    :return: 最好特征的索引值
    """
    numFeatures = len(dataSet[0]) - 1#特征数量
    baseEntropy = calcShannonEntropy(dataSet)#计算数据集的香农熵
    bestInfoGain = 0.0#信息增益
    bestFeature = -1#最优特征的索引值
    for i in range(numFeatures):#遍历所有特征
        #获取dataSet的第i个所有特征
        featList = [example[i] for example in dataSet]
        #print("featList:",featList)
        uniqueVals = set(featList)#创建set集合{},元素不可重复
        #print(uniqueVals)
        newEntropy = 0.0#经验条件熵
        for value in uniqueVals:#计算信息增益
            #print("i:",i,",value:",value)
            subDataSet = splitDataSet(dataSet, i, value)#subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet))#计算子集的概率
            newEntropy += prob * calcShannonEntropy(subDataSet)#根据公式计算经验条件熵
        infoGain = baseEntropy - newEntropy#信息增益
        #print("第%d个特征的增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
        if (infoGain > bestInfoGain):#计算信息增益
            bestInfoGain = infoGain#更新信息增益,找到最大的信息增益
            bestFeature = i#记录信息增益最大的特征的索引值
    return bestFeature#返回信息增益最大的特征的索引值

def majorityCnt(classList):
    """
    统计类标签中出现最多的元素
    :param classList:类标签列表
    :return: 出现最多的元素
    """
    classCount = {}#统计classList中每个元素出现的次数
    for vote in classList:
        if vote not in classCount.keys():classCount[vote] = 0
        classCount += 1
    #根据字典值的降序排序
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet,labels,featLabels):
    """
    构造决策树
    :param dataSet:训练数据集 
    :param labels:分类属性标签 
    :param featLabels:储存选择的最优特征标签 
    :return: 决策树
    """
    classList = [example[-1] for example in dataSet]#取分类标签
    print("classList:",classList)
    #如果类别完全相同则停止继续划分
    if classList.count(classList[0]) == len(classList):
        print("类别完全相同")
        return classList[0]
    #遍历完所有特征时返回出现次数最多的类标签
    if len(dataSet[0]) == 1:
        print("遍历完所有特征")
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)#选择最优特征
    print("bestFeat:",bestFeat)
    bestFeatLabel = labels[bestFeat]#最优特征的标签
    print("bestFeatLabel:", bestFeatLabel)
    featLabels.append(bestFeatLabel)
    print("featLabels",featLabels)
    myTree = {bestFeatLabel: {}}#据最优特征的标签生成树
    print("myTree",myTree)
    del (labels[bestFeat])#删除已经使用特征标签
    print("labels:",labels)
    featValues = [example[bestFeat] for example in dataSet]#得到训练集中所有最优特征的属性值
    uniqueVals = set(featValues)#去掉重复的属性值
    print("uniqueVals",uniqueVals)
    for value in uniqueVals:#遍历特征,创建决策树。
        print("value:",value)
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
        print("myTree:",myTree)
    return myTree

if __name__ == '__main__':
    dataSet,labels = creatDataset()
    featLabels = []
    myTree = createTree(dataSet,labels,featLabels)
    print(myTree)

运行结果


决策树可视化

使用matplotlib实现决策树可视化

下面介绍需要使用到的API

getNumLeafs:获取决策树叶子节点的数目

getTreeDepth:获取决策树的层数

plotNode:绘制节点

plotMidText:标注有向属性值

plotTree:绘制决策树

createPlot:创建绘制面板

绘制决策树代码(本文代码参照点击打开链接

#绘制决策树
def getNumLeafs(myTree):
    """
    获取决策树叶子节点数目
    :param myTree: 
    :return:决策树叶子节点数目 
    """
    numLeafs = 0
    #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0]
    firstStr = next(iter(myTree))
    #print("firstStr:",firstStr)
    secondDict = myTree[firstStr]#获取下一组字典
    #print("secondDict:",secondDict)
    for key in secondDict.keys():
        #测试该节点是否是字典,如果不是则说明为叶子节点
        if type(secondDict[key]).__name__ == 'dict':
            #print("字典")
            numLeafs += getNumLeafs(secondDict[key])
        else:
            #print("叶子节点")
            numLeafs += 1
    #print("numLeafs:",numLeafs)
    return numLeafs

def getTreeDepth(myTree):
    """
    获取决策树的深度
    :param myTree: 
    :return:决策树的深度 
    """
    maxDepth = 0
    firstStr = next(iter(myTree))
    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 plotNode(nodeTxt, centerPt, parentPt, nodeType):
    arrow_args = dict(arrowstyle="<-")#定义箭头格式
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)#设置中文字体
    #绘制结点
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',xytext=centerPt, textcoords='axes fraction',va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)

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, va="center", ha="center", rotation=30)

def plotTree(myTree, parentPt, nodeTxt):
    decisionNode = dict(boxstyle="sawtooth", fc="0.8")#设置结点格式
    leafNode = dict(boxstyle="round4", fc="0.8")#设置叶结点格式
    numLeafs = getNumLeafs(myTree)#获取决策树叶结点数目,决定了树的宽度
    depth = getTreeDepth(myTree)#获取决策树层数
    firstStr = next(iter(myTree))#下个字典
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)#中心位置
    plotMidText(cntrPt, parentPt, nodeTxt)#标注有向边属性值
    plotNode(firstStr, cntrPt, parentPt, decisionNode)#绘制结点
    secondDict = myTree[firstStr]#下一个字典,也就是继续绘制子结点
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD#y偏移
    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
    fig.clf()#清空fig
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)#去掉x、y轴
    plotTree.totalW = float(getNumLeafs(inTree))#获取决策树叶结点数目
    plotTree.totalD = float(getTreeDepth(inTree))#获取决策树层数
    plotTree.xOff = -0.5/plotTree.totalW
    plotTree.yOff = 1.0#x偏移
    plotTree(inTree, (0.5,1.0), '')#绘制决策树
    plt.show()

运行结果


利用决策树进行分类

def classify(inputTree,featLabels,testVec):
    """
    使用决策树分类
    :param inputTree: 已经生成的决策树
    :param featLabels: 存储选择的最优特征标签
    :param testVec: 测试数据列表
    :return: 分类结果
    """
    firstStr = next(iter(inputTree))
    print("firstStr:",firstStr)
    secondDict = inputTree[firstStr]
    print("secondDict:",secondDict)
    featIndex = featLabels.index(firstStr)
    print("featIndex:",featIndex)
    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

if __name__ == '__main__':
    dataSet,labels = creatDataset()
    featLabels = []
    myTree = createTree(dataSet,labels,featLabels)
    print(myTree)
    testVec = [0,1]
    print("featLabels",featLabels)
    result = classify(myTree,featLabels,testVec)
    if result == 'yes':
        print("放贷")
    if result == 'no':
        print("不放贷")
    createPlot(myTree)

运行结果


存储决策树

如果每次使用分类器都创建一次决策树逻辑上说不通,所以需要对决策树进行存储

存储和载入决策树的Python代码

def storeTree(inputTree,filename):
    """
    保存决策树
    :param inputTree: 要保存的决策树
    :param filename: 保存的文件名
    :return: None
    """
    with open(filename,'wb') as fw:
        pickle.dump(inputTree,fw)

def grabTree(filename):
    """
    导入决策树
    :param filename: 保存的决策树文件名
    :return: 决策树
    """
    fr = open(filename,'rb')
    return pickle.load(fr)

if __name__ == '__main__':
    # dataSet,labels = creatDataset()
    # featLabels = []
    # myTree = createTree(dataSet,labels,featLabels)
    # print(myTree)
    #保存决策树
    #storeTree(myTree,'classifierStorage.txt')

    #导入决策树
    print(grabTree('classifierStorage.txt'))


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值