机器学习_3:决策树的构建及应用

文章目录

  • 实验背景
  • 1.决策树算法原理
    • 1.1.什么是决策树
    • 1.2.如何构建好的决策树
      • 1.2.1.香浓熵
      • 1.2.2.信息增益
      • 1.2.3.信息增益率
      • 1.2.4.基尼指数
    • 1.3.如何优化构建完的决策树
      • 1.3.1.预剪枝
      • 1.3.2.后剪枝
  • 2.三种决策树构造
    • 2.1.基于信息增益构造的决策树(ID 3)
    • 2.2.基于信息增益率构造的决策树(C 4.5)
    • 2.3.基于基尼指数构造的决策树(CART)
  • 3.决策树测试
    • 3.1.ID 3
    • 3.2.C 4.5
    • 3.3.CART
  • 4.总结

实验背景

在之前的实验:
机器学习_1:K-近领算法
机器学习_2:K-近领算法
我们了解到了K-近邻算法是一种无需训练就可以拿来进行分类的算法,但缺点也很多,最大的缺点就是无法给出数据的内在含义,今天我就来介绍一种容易理解数据形式的分类算法——决策树算法。

1.决策树算法原理

1.1.什么是决策树

严格定义上来说,分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点和叶节点。内部结点表示一个特征或属性,叶节点表示一个类。
通俗来说决策树只有一个功能——判断,通过不断的判断最终得出一个结果,以相亲为例
机器学习_3:决策树的构建及应用_第1张图片
我们将相亲简单的化为三个属性的思考(对象白不白,富不富,美不美),实际上的相亲远比这复杂,很显然,白富美的对象几乎没有人会拒绝。而一个新的对象放进决策树里判断,这个对象是白,富,不美,经过3次判断之后,得出的结论是去。这就是决策树,当然,这种’海王’决策树不是我们特别需要的。
决策树的使用更多倾向于专家系统中,以病情诊断为例,将以前人们记录的各种疾病以及这些疾病发生时的症状记录下来,用于构建决策树,经过这些疾病的训练后,当病人根据提示输入体温等各种信息后,这个病情决策树就能判断出患者得了哪种病。
那么问题就来了,哪种属性作为判断的依据呢,或者说,哪种属性作为判断依据可以有最好的效果呢?

1.2.如何构建好的决策树

想要构造好的决策树,就得找到合适的属性作为判断依据,如何选择合适的属性呢,这里引入四个概念,香浓熵,信息增益,信息增益率,基尼指数。

1.2.1.香浓熵

熵,本质是指一个系统的“内在混乱程度”,香浓熵也一样,这个词用来形容一个事物的确定程度,香浓熵越大,事物的就越不确定,香浓熵越小,事物越确定。
公式为:
机器学习_3:决策树的构建及应用_第2张图片
Ent(D)表示熵,pk表示k事件出现的概率,k=1~y表示共有y种事件
以硬币为例,一枚硬币抛出,正面或反面朝上的概率均为50%,此时熵为:-(1/2(log2(1/2))+1/2(log2(1/2)))=1。很显然,此时就是这个硬币最不确定的时候,如果我们对硬币作手脚,让其概率发生变化,香浓熵也会随之变化。

机器学习_3:决策树的构建及应用_第3张图片
这就是硬币概率变化导致熵的变化,当然,这里硬币只有两种结果,当这两种概率相等的时候熵最大。当事件越多,熵值就会越大。

1.2.2.信息增益

在划分数据集之前之后信息发生的变化称为信息增益,计算公式为:
机器学习_3:决策树的构建及应用_第4张图片
Gain(D,a)表示集合D中属性a的信息增益,Dv表示属性a有V种分支,第v个节点包含了D在a属性上取值为av的所有样本。Gain(D,a)越大,代表根据属性a划分获得的“纯度提升”越大。

1.2.3.信息增益率

虽然信息增益可以帮助我们选择合适的划分属性,但他也有一个很明显的问题,那就是明显偏好可取数目比较多的属性,以硬币为例,如果抛10次硬币,5次朝上,5次朝下,如果以次数为属性,可以发现,每次都只有一个值即属性是确定的,这样信息增益就会变成1-0=1。所以我们引入信息增益率
机器学习_3:决策树的构建及应用_第5张图片
IV(a)称为属性a的固有值,属性a的可能取值越多,IV(a)的值通常就越大,显然,这种方法也有一个缺点,那就是偏好可取数目比较少的属性,所以我们先挑选信息增益高于平均水平的属性,再对其求信息增益率,这样就能得到合适的属性。

1.2.4.基尼指数

基尼指数是不同于信息增益和信息增益率的另一套属性,他用来描述数据集的纯度,反应了随机抽取两个样本,其类别标记不一致的概率,也就是基尼指数越小,这个数据集的标记越统一
公式为:
在这里插入图片描述
给定数据集D,属性a的基尼指数定义为:
在这里插入图片描述
在候选属性集合A中,选择那个使得划分后基尼指数最小的属性作为最有划分属性。

1.3.如何优化构建完的决策树

尽管有多种方法可以选择合适的属性来划分数据集,构造决策树,但根据训练数据集训练出来的决策树也有很明显的问题,那就是过度拟合训练数据,导致泛用性下降。实际上,训练数据集不可能包含了世界上的所有情况,这就导致一个新的样例丢入决策树进行判断,得出的结果有很大可能不是我们想要的,因为这种样例决策树没有接触过。这就需要我们对决策树进行“剪枝”。

1.3.1.预剪枝

顾名思义,预剪枝通过提前停止树的构建而对树剪枝,主要方法有4种:
1.当决策树达到预设的高度时就停止决策树的生长。
2.达到某个节点的实例具有相同的特征向量,即使这些实例不属于同一类,也可以停止决策树的生长。
(比如同样是头疼,发热,大部分是感冒,小部分是中暑,我们就认为头疼,发热都是感冒)
3.定义一个阈值,当达到某个节点的实例个数小于阈值时就可以停止决策树的生长。
(这是为了防止过度拟合训练数据,比如头疼的样例有10个,头疼发热的样例只有1个,那就无需再用发热进行划分)
4.通过计算每次扩张对系统性能的增益,决定是否停止决策树的
(提升系统性能就扩张,降低就砍掉这个属性)
方法3中有个很明显的问题,这个阈值是我们事先规定的,但是如何确定这个阈值是值得讨论的,如何控制好欠拟合-过拟合的平衡是这个地方的难点。
尽管预剪枝能降低过拟合风险并且显著减少训练时间和测试时间开销。但因为他的“贪心”导致可能会陷入局部最优,也就是无法解决高-低-更高的情况,会困死在小山包上。同时因为限制分枝的展开,使得欠拟合风险大大提高(毕竟头疼发热的样例哪怕只有一例,如果是病毒的情况,这种也是需要注意的,预剪枝就可能会导致忽视这种问题)
机器学习_3:决策树的构建及应用_第6张图片
如图所示,预剪枝情况下是没有上面那棵完整的决策树的,直接画出下面那棵剪枝后的决策树。

1.3.2.后剪枝

后剪枝先从训练集生成一棵完整的决策树,然后自底向上地对非叶结点进行分析计算,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点。如果没有提升,保持不便的话,我们就保留。
优缺点都很明显,保留更多的分支使得欠拟合的风险减小,有足够分支的情况下一般也比分支过少的预剪枝泛化性更好;更优秀的表现往往需要更多的付出,因为这种方法是生成完整的决策树后,自底向上进行剪枝,导致训练时间很长,开销大。
机器学习_3:决策树的构建及应用_第7张图片
后剪枝先生成上面那棵完整的决策树,再对那棵决策树进行剪枝。

2.三种决策树构造

原理解释完后,我们来分别构建基于信息增益,信息增益率,基尼指数的决策树,首先,我们需要计算香浓熵,代码如下:

#计算香浓熵
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)                       #获取数据集总数
    labelCounts = {}                                #字典,存储类别及个数
    for featVec in dataSet:                         #dataSet进入后为list形式,使用变量featVec进行列表遍历
        currentLabel = featVec[-1]                  #取出列表最后一个标签
        if currentLabel not in labelCounts.keys():  #若标签不在字典中
            labelCounts[currentLabel] = 0           #初始化标签数量
        labelCounts[currentLabel] += 1              #在字典中就数量+1
    shannonEnt = 0.0                                #初始化香浓熵
    for key in labelCounts:                         #使用关键字循环遍历
        prob = float(labelCounts[key])/numEntries   #将频数转换成概率
        shannonEnt -= prob * log(prob,2)            #公式求熵
    return shannonEnt                               #返回香浓熵
#测试用数据
def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing','flippers']
    #change to discrete values
    return dataSet, labels

机器学习_3:决策树的构建及应用_第8张图片

2.1.基于信息增益构造的决策树(ID 3)

#按照给定的特征划分数据集
def splitDataSet(dataSet, axis, value):
    retDataSet = []                                 #存储分割数据
    for featVec in dataSet:                         #list遍历
        if featVec[axis] == value:                  #如果数据集里的value符合我们给定的axis
            reducedFeatVec = featVec[:axis]         #将该数据加入reducedFeatVec
            reducedFeatVec.extend(featVec[axis+1:]) #用extend追加多个值
            retDataSet.append(reducedFeatVec)       #将reducedFeatVec通过append添加给retDataSet
    return retDataSet                               #返回划分数据

#选择最好的数据集划分方式(信息增益)
def chooseBestFeatureToSplit1(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
        #0/1分类,遍历
        for value in uniqueVals:
            #将0,1实例分别计算条件熵
            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                          #返回特征下标

#找出出现次数最多的标签,用于投票
def majorityCnt(classList):
    classCount={}
    #记录每个标签的出现数量
    for vote in classList:
        if vote not in classCount.keys(): classCount[vote] = 0
        classCount[vote] += 1
    #降序排序找出出现次数最多的标签
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

#构建决策树
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]        #-1表示列表最后一个
    if classList.count(classList[0]) == len(classList):     #所有结果一样就无需再次分类
        return classList[0]                                 #返回结果
    if len(dataSet[0]) == 1:                                #只剩下一种结果时也无需分类
        return majorityCnt(classList)                       #返回出现次数最多的类
    #使用信息增益选择最佳划分方式
    bestFeat = chooseBestFeatureToSplit1(dataSet)
    bestFeatLabel = labels[bestFeat]
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat])                                   #从labels中删除根节点
    featValues = [example[bestFeat] for example in dataSet] 
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]                               #复制所有标签,避免弄乱现有标签
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree

在这里插入图片描述
这样的决策树明显不够直观,我们加点绘制图像。

import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
#获取叶子节点
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':#test to see if the nodes are dictonaires, if not they are leaf nodes
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth

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 )
    
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):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]     #the text label for this node should be this
    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
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes   
            plotTree(secondDict[key],cntrPt,str(key))        #recursion
        else:   #it's a leaf node print the leaf node
            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
#if you do get a dictonary you know it's a tree, and the first element will be another dict

def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
    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 createPlot():
#    fig = plt.figure(1, facecolor='white')
#    fig.clf()
#    createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
#    plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
#    plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
#    plt.show()

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]

#createPlot(thisTree)

机器学习_3:决策树的构建及应用_第9张图片

2.2.基于信息增益率构造的决策树(C 4.5)

#选择最好的数据集划分方式(信息增益率)
def chooseBestFeatureToSplit2(dataSet):
    numFeatures = len(dataSet[0]) - 1      #去除标签的特征数量
    baseEntropy = calcShannonEnt(dataSet)  #得到香浓熵
    #初始化参数
    bestInfoGainration = 0.0
    bestFeature = -1
    sum=0.0
    m=0
    #获取平均信息增益
    for j in range(numFeatures):
        #统计数量
        m=m+1
        featList = [example[j] for example in dataSet]  #创建列表存放特征样例
        uniqueVals = set(featList)                      #删除重复数据,获取唯一数据
        newEntropy = 0.0
        IV=0.0
        #0/1分类,遍历
        for value in uniqueVals:
            #将0,1实例分别计算条件熵
            subDataSet = splitDataSet(dataSet, j, value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)     #条件熵
            IV-=prob * log(prob,2)
        infoGain = baseEntropy - newEntropy     #信息增益
        sum+=infoGain
    avg=sum/m
    for i in range(numFeatures):           #迭代所有特征
        featList = [example[i] for example in dataSet]  #创建列表存放特征样例
        uniqueVals = set(featList)                      #删除重复数据,获取唯一数据
        newEntropy = 0.0
        IV=0.0
        #0/1分类,遍历
        for value in uniqueVals:
            #将0,1实例分别计算条件熵
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)     #条件熵
            IV-=prob * log(prob,2)
        infoGain = baseEntropy - newEntropy     #信息增益
        if IV==0.0:
            infoGainration=0.0
        else:
            infoGainration=float(infoGain)/float(IV)
        if (infoGainration > bestInfoGainration and infoGainration>avg):           #选择最大的信息增益率
            bestInfoGainration = infoGainration         
            bestFeature = i
    return bestFeature                          #返回特征下标

C 4.5的代码和ID 3很像,核心区别只在于如何选择信息收益大于平均收益的情况下的信息增益率。
机器学习_3:决策树的构建及应用_第10张图片
微量数据下无法看出明显区别

2.3.基于基尼指数构造的决策树(CART)

#计算基尼指数
def calcGini(dataset):
    feature = [example[-1] for example in dataset]
    uniqueFeat = set(feature)
    sumProb =0.0
    for feat in uniqueFeat:
        prob = feature.count(feat)/len(uniqueFeat)
        sumProb += prob*prob
    sumProb = 1-sumProb
    return sumProb

def chooseBestFeatureToSplit3(dataSet): #使用基尼系数进行划分数据集
    numFeatures = len(dataSet[0]) -1 #最后一个位置的特征不算
    bestInfoGini = 1
    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 *calcGini(subDataSet)
        infoGini = newEntropy 
        if(infoGini < bestInfoGini): # 选择最小的基尼系数作为划分依据
            bestInfoGini = infoGini
            bestFeature = i
    return bestFeature #返回决策属性的最佳索引

机器学习_3:决策树的构建及应用_第11张图片
微量数据下没有区别。

3.决策树测试

对于大量的数据集,为了方便存储与复用,我们需要保存在硬盘内

#存储文件
def saveTree(inputTree,filename):
    fw = open(filename,'w')
    pickle.dump(inputTree,fw)
    fw.close()
#读取文件
def loadTree(filename):
    fr = open(filename)
    return pickle.load(fr)
if __name__ == '__main__':
    fr=open('D:/vscode/python/.vscode/car.data','r')
    dataSet=[inst.strip().split(',') for inst in fr.readlines()]
    Labels=['buying','maint','doors','persons','lug_boot','safety']
    trainLabels=['buying','maint','doors','persons','lug_boot','safety']
    train_num = int(len(dataSet)*0.8) #使用百分之八十得到数据进行训练,百分之二十数据验证
    train_data = dataSet[:train_num]
    test_data = dataSet[train_num+1:]
    trainTree = createTree(train_data,Labels) #利用数据集标签创建二叉树
    createPlot(trainTree)
    errCount = 0.0
    for data in test_data:
        testVec = data[:-1]
        result = classify(trainTree,trainLabels,testVec) #获得分类结果
        if result!=data[-1]: #统计错误个数
            errCount+=1.0
    prob = (1-(errCount/len(test_data)))*100 #计算准确率
    print(prob)

3.1.ID 3

机器学习_3:决策树的构建及应用_第12张图片
机器学习_3:决策树的构建及应用_第13张图片
准确度为68.7%

3.2.C 4.5

机器学习_3:决策树的构建及应用_第14张图片
在这里插入图片描述
准确度为68.1%,但是决策树节点少了很多

3.3.CART

机器学习_3:决策树的构建及应用_第15张图片
在这里插入图片描述
准确度只有65.8%

4.总结

通过此次决策树的学习和代码的运行,我对决策树有了更深刻的体会与理解。决策树的算法不需要调整过多的参数,同时算法的可解释性也非常的强。决策树的缺点也十分明显,当决策属性过多,整个决策树的算法的开销将会非常大。同时经过多次测试数据集,可以发现当决策属性为连续类型数据时,容易导致决策树分枝过多,此时需要将连续性数据离散化。

你可能感兴趣的:(决策树,机器学习,人工智能)