【机器学习】决策树算法--2(代码模块实现)

文章目录

  • 接之前的【机器学习】决策树算法--1(算法介绍)
    • 三好学生评选表进行代码实例
      • 1、整体决策树模块(先从离散型数据开始)
      • 2、可视化决策树
      • 3、将数据集增加一列连续型数据
        • 3.1、离散型与连续型处理区别
        • 3.2、注意事项
      • 4、预剪枝介绍及实现
        • 预剪枝思考:
      • 5、后剪枝介绍及实现
        • 后剪枝相比于预剪枝:
        • 预剪枝实现:
        • 后剪枝实现
      • 6、总结

接之前的【机器学习】决策树算法–1(算法介绍)

三好学生评选表进行代码实例

1、整体决策树模块(先从离散型数据开始)

                            集美大学三好学生评选表
是否挂科 获得奖学金次数 综测评价 体质健康是否达标 宿舍检评 是否符合条件
1 no 4 excellect yes excellent yes
2 no 1 good no excellent no
3 no 0 excellect yes excellent yes
4 no 1 excellect no excellent no
5 no 2 good yes excellent yes
6 no 1 excellect yes excellent no
7 no 1 excellect yes excellent yes
8 yes 0 good yes excellent no
9 no 2 good yes good no
10 no 2 excellect yes excellent yes
11 yes 2 excellect yes excellent no
12 yes 0 good yes good no
13 yes 0 excellect yes pass no
14 no 4 excellect yes excellent yes
15 no 2 excellect yes excellent yes

【机器学习】决策树算法--2(代码模块实现)_第1张图片

def createDataSet1():    # 创造示例数据
    dataSet = [['no', '4','excllent', 'yes','excllent','yes'],
               ['no', '1', 'good','no','excllent','no'],
               ['no', '0', 'excllent','yes','excllent','yes'],
               ['no', '1', 'excllent','no','excllent','no'],
               ['no', '2', 'good','yes','excllent','yes'],
               ['no', '1', 'excllent','yes','excllent','no'],
               ['no', '1', 'excllent','yes','excllent','yes'],
               ['yes', '0', 'good','yes','excllent','no'],
               ['no', '2', 'good','yes','good','no'],
               ['no', '2', 'excllent','yes','excllent','yes'],
               ['yes', '2', 'excllent','yes','excllent','no'],
               ['yes', '0', 'good','yes','good','no'],
               ['yes', '0', 'excllent','yes','pass','no'],
               ['no', '4', 'excllent','yes','excllent','yes'],
               ['no', '2', 'excllent','yes','excllent','yes']]
    labels = ['Failclass','Scholarship-num','Grade-ranking','Physically-fit','Hostel-assessment'] 
    #是否挂科,奖学金次数,综测分评价,体质健康是否符合标准,宿舍检评
    return dataSet,labels
 


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]#找到对应的标签
    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(splitDataSet\
                            (dataSet,bestFeat,value),subLabels)#在当前最好节点下继续做,递归创建添加最好的节点下面,splitDataSet切分后的数据集
    return myTree
 
def majorityCnt(classList):    #返回最多类别
    classCount={}
    for vote in classList:
        if vote not in classCount.keys():#如果说这个vote不在那赋值为0
            classCount[vote]=0
        classCount[vote]+=1#在就加等于一
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)#排序后的
    return sortedClassCount[0][0]

def chooseBestFeatureToSplit(dataSet):  #选择最优的特征
    numFeatures = len(dataSet[0])-1#当前特征数量,要减去labels
    baseEntropy = calcShannonEnt(dataSet)  #基础的熵值,啥都没做的时候的熵值
    bestInfoGain = 0#最好的信息增益
    bestFeature = -1#最好的特征
    for i in range(numFeatures):#遍历特征列
        featList = [example[i] for example in dataSet]#得到当前列的特征
        uniqueVals = set(featList)#得到唯一的个数
        newEntropy = 0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet,i,value)
            prob =len(subDataSet)/float(len(dataSet))#看下去掉列后占总体的比值,后面要用到剩下的占总体的概率值
            newEntropy +=prob*calcShannonEnt(subDataSet) #计算累加后面新的熵值,对每一个特征进行操作
            print("信息熵:%f"  %newEntropy)
        infoGain = baseEntropy - newEntropy #信息增益
        print("信息增益:%f"  %infoGain)
        if (infoGain>bestInfoGain):   
            bestInfoGain=infoGain
            bestFeature = i#选最好的特征
    return bestFeature
 
def calcShannonEnt(dataSet):  # 计算数据的熵(entropy)
    numEntries=len(dataSet)  # 所有的样本个数
    labelCounts={}#看labels里面的类出现的次数
    for featVec in dataSet:
        currentLabel=featVec[-1] # 每行样本的最后一个labels
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel]=0#因为第一次都没在这个里面
        labelCounts[currentLabel]+=1  #在的话就统计,分别有多少个
    shannonEnt=0
    for key in labelCounts:
        prob=float(labelCounts[key])/numEntries # 计算labels中的一个类的熵值
        shannonEnt-=prob*log(prob,2) # 累加每个类的熵值,按照那个信息熵的公式
    return shannonEnt

def splitDataSet(dataSet,axis,value): #切分数据集,
    retDataSet=[]
    for featVec in dataSet:#遍历样本
        if featVec[axis]==value:#要从传进来的value列找到labels里面是同一个值,同个类
            reducedFeatVec =featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])#直接把featVec[axis]这一列就给去掉了
            retDataSet.append(reducedFeatVec)#要删除正在遍历的这一列
    return retDataSet
 

对上述模型进行输出:

if __name__ == '__main__':
    dataSet, labels=createDataSet1()  # 创造示列数据
    myTree = createTree(dataSet, labels)
    print(myTree)  # 输出决策树模型结果
    

运行得到:
在这里插入图片描述

【机器学习】决策树算法--2(代码模块实现)_第2张图片

2、可视化决策树

#获取叶节点的数目
def getNumLeafs(myTree):
    # 定义叶子结点数目
    numLeaf=0
    # 获得myTree的第一个键值,即第一个特征,分割的标签
    firstStr=list(myTree.keys())[0]
    # 根据键值得到对应的值,即根据第一个特征分类的结果
    secondDict=myTree[firstStr]
    # 遍历得到的secondDict
    for key in secondDict.keys():
        # 如果secondDict[key]为一个字典,即决策树结点,type()可以判断子节点是否为字典类型
        if type(secondDict[key]).__name__=='dict':
            # 则递归的计算secondDict中的叶子结点数,并加到numLeafs上
            numLeaf+=getNumLeafs(secondDict[key])
        # 如果secondDict[key]为叶子结点
        else:
            # 则将叶子结点数加1
            numLeaf+=1
    # 返回求的叶子结点数目
    return numLeaf

#获取树的层数
def getTreeDepth(myTree):
    # 定义树的深度
    maxDepth=0
    # 获得myTree的第一个键值,即第一个特征,分割的标签
    firstStr=list(myTree.keys())[0]
    # 根据键值得到对应的值,即根据第一个特征分类的结果
    secondDict=myTree[firstStr]
    for key in secondDict.keys():
        # 如果secondDict[key]为一个字典
        if type(secondDict[key]).__name__=='dict':
            # 则当前树的深度等于1加上secondDict的深度,只有当前点为决策树点深度才会加1
            thisDepth=1+getTreeDepth(secondDict[key])
        # 如果secondDict[key]为叶子结点
        else:
            # 则将当前树的深度设为1
            thisDepth=1
        # 比较当前树的深度与最大数的深度
        if thisDepth>maxDepth:
            maxDepth=thisDepth
    # 返回树的深度
    return maxDepth

#预先存储树的信息
#def retrieveTree(i):
    listOfTree=[{'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 listOfTree[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,nodeTxt):
    # 定义并获得决策树的叶子结点数
    numLeafs=getNumLeafs(myTree)
    depth=getTreeDepth(myTree)
    # 得到第一个特征
    firstStr=list(myTree.keys())[0]
    # 计算坐标,x坐标为当前树的叶子结点数目除以整个树的叶子结点数再除以2,y为起点
    cntrPt=(plotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,plotTree.yOff)
    # 绘制中间结点,即决策树结点,也是当前树的根结点
    plotMidText(cntrPt,parentPt,nodeTxt)
    # 绘制决策树结点
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    # 根据firstStr找到对应的值
    secondDict=myTree[firstStr]
    # 因为进入了下一层,所以y的坐标要变 ,图像坐标是从左上角为原点
    plotTree.yOff=plotTree.yOff-1.0/plotTree.totalD
    # 遍历secondDict
    for key in secondDict.keys():
        # 如果secondDict[key]为一棵子决策树,即字典
        if type(secondDict[key]).__name__=='dict':
            # 递归的绘制决策树
            plotTree(secondDict[key],cntrPt,str(key))
        # 若secondDict[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保存的是树的宽
    plotTree.totalW=float(getNumLeafs(inTree))
    # plotTree.totalD保存的是树的高
    plotTree.totalD=float(getTreeDepth(inTree))
    # 决策树起始横坐标
    plotTree.xOff=-0.5/plotTree.totalW
    # 决策树的起始纵坐标
    plotTree.yOff=1.0
    # 绘制决策树
    plotTree(inTree,(0.5,1.0),'')
    # 显示图像
    plt.show()

对上述可视化操作运行:
【机器学习】决策树算法--2(代码模块实现)_第3张图片

运行得到:
【机器学习】决策树算法--2(代码模块实现)_第4张图片

3、将数据集增加一列连续型数据

我是直接在后面再加上一个特征属性获奖率,是个连续型数据
【机器学习】决策树算法--2(代码模块实现)_第5张图片

3.1、离散型与连续型处理区别

通过将连续型转换成离散型,再按照上述代码进行。就是以这个集美大学三好学生评选表为例,先从小到大排序,再从第一个开始在第1,2个样本的获奖率切一刀,然后左边只有一个0.089,右边14个,这是计算获奖率小于0.089的Gain(D,获奖率<0.089),这是第一个信息增益,再在第2,3个样本来一刀,左边两个右边13个,计算Gain(D,获奖率<0.101),以此类推,求初最后Gain(D,获奖率<651),然后再选出最大的信息增益,从而选出最合适的分割点。具体代码修改如下:

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=''#得到最好特征的名称

    # 记录此刻是连续值还是离散值,1连续,2离散
    flagSeries = 0

    # 如果是连续值,记录连续值的划分点
    midSeries = 0.0

    # 如果是元组的话,说明此时是连续值
    if isinstance(bestFeat, tuple):
        # 重新修改分叉点信息
        bestFeatLabel = str(labels[bestFeat[0]]) + '小于' + str(bestFeat[1]) + '?'
        # 得到当前的划分点
        midSeries = bestFeat[1]
        # 得到下标值
        bestFeat = bestFeat[0]
        # 连续值标志
        flagSeries = 1
    else:
        # 得到分叉点信息
        bestFeatLabel = labels[bestFeat]
        # 离散值标志
        flagSeries = 0

    myTree={bestFeatLabel:{}} #字典嵌套字典,对应根节点下面的节点,第一次是根节点,之后嵌套节点
    
    featValues=[example[bestFeat] for example in dataSet]#统计里面有多少个相同的属性,就是分多少个树杈

    # 连续值处理
    if flagSeries:
        # 将连续值划分为不大于当前划分点和大于当前划分点两部分
        eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, bestFeat, midSeries)
        # 得到剩下的特征标签
        subLabels = labels[:]
        # 递归处理小于划分点的子树
        subTree = createTree(eltDataSet, subLabels)
        myTree[bestFeatLabel]['小于'] = subTree

        # 递归处理大于当前划分点的子树
        subTree = createTree(gtDataSet, subLabels)
        myTree[bestFeatLabel]['大于'] = subTree

        return myTree

    # 离散值处理
    else:
        del(labels[bestFeat])#嵌套一个节点后要删掉,列名
        uniqueVals=set(featValues)
        for value in uniqueVals:
            subLabels=labels[:]#去掉一列的标签
            myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLabels)#在当前最好节点下继续做,递归创建添加最好的节点下面,splitDataSet切分后的数据集
        return myTree

最优解特征就是上述连续型数据处理的过程,分成两个小于分割点的和大于分割点的之后在递归按照离散的处理就可以了。

def chooseBestFeatureToSplit(dataSet):  #选择最优的特征
    numFeatures = len(dataSet[0])-1#当前特征数量,要减去labels
    baseEntropy = calcShannonEnt(dataSet)  #基础的熵值,啥都没做的时候的熵值
    flagSeries = 0# 标记当前最好的特征值是不是连续值
    bestSeriesMid = 0.0# 如果是连续值的话,用来记录连续值的划分点
    bestInfoGain = 0#最好的数据列
    bestFeature = -1#最好的特征
    for i in range(numFeatures):#遍历特征列
        featList = [example[i] for example in dataSet]#得到当前列的特征
        if isinstance(featList[0], str):
            uniqueVals = set(featList)#得到唯一的个数
            newEntropy = 0
            for value in uniqueVals:
                subDataSet = splitDataSet(dataSet,i,value)
                prob =len(subDataSet)/float(len(dataSet))#看下去掉列后占总体的比值,后面要用到剩下的占总体的概率值
                newEntropy +=prob*calcShannonEnt(subDataSet) #计算累加后面新的熵值,对每一个特征进行操作
                #print("信息熵:%f"  %newEntropy)
            infoGain = baseEntropy - newEntropy #信息增益
            #print("信息增益:%f"  %infoGain)
            
        else:
            maxInfoGain = 0.0 # 记录最大的信息增益  
            bestMid = -1# 最好的划分点
            featList = [example[i] for example in dataSet]# 得到数据集中所有的当前特征值列表
            classList = [example[-1] for example in dataSet]# 得到分类列表
            dictList = dict(zip(featList, classList))
            sortedFeatList = sorted(dictList.items(), key=operator.itemgetter(0)) # 将其从小到大排序,按照连续值的大小排列
            numberForFeatList = len(sortedFeatList)# 计算连续值有多少个
            midFeatList = [round((sortedFeatList[i][0] + sortedFeatList[i+1][0])/2.0, 3)for i in range(numberForFeatList - 1)]# 计算划分点,保留三位小数                                                                                                                                
            # 计算出各个划分点信息增益
            for mid in midFeatList:  
                eltDataSet, gtDataSet = splitDataSetForSeries(dataSet, i, mid)# 将连续值划分为不大于当前划分点和大于当前划分点两部分                                                                                                                                                                                                                                                        
                newEntropy = len(eltDataSet)/len(sortedFeatList)*calcShannonEnt(eltDataSet) + len(gtDataSet)/len(sortedFeatList)*calcShannonEnt(gtDataSet)# 计算两部分的特征值熵和权重的乘积之和 
                infoGain = baseEntropy - newEntropy # 计算出信息增益
                if infoGain > maxInfoGain:
                    bestMid = mid
                    maxInfoGain = infoGain

        print('当前特征值为:' + labels[i] + ',对应的信息增益值为:' + str(infoGain))
        if infoGain > bestInfoGain:
            # 最好的信息增益
            bestInfoGain = infoGain
            # 新的最好的用来划分的特征值
            bestFeature = i
            flagSeries = 0
            if not isinstance(dataSet[0][bestFeature], str):
                flagSeries = 1
                bestSeriesMid = bestMid
    print('信息增益最大的特征为:' + labels[bestFeature])

    if flagSeries:
        return bestFeature, bestSeriesMid
    else:
        return bestFeature
 

也新添加了切分连续数据集的方法

def splitDataSetForSeries(dataSet, axis, value):
    """
    按照给定的数值,将数据集分为不大于和大于两部分
    :param dataSet: 要划分的数据集
    :param i: 特征值所在的下标
    :param value: 划分值
    :return:
    """
    eltDataSet = []# 用来保存不大于划分值的集合
    gtDataSet = []# 用来保存大于划分值的集合
    # 进行划分,保留该特征值
    for feat in dataSet:
        if feat[axis] <= value:
            eltDataSet.append(feat)
        else:
            gtDataSet.append(feat)

    return eltDataSet, gtDataSet

最后运行得到如下图:
在选择最有特征方法适当print,使结果更易看出。

【机器学习】决策树算法--2(代码模块实现)_第6张图片

可视化:
【机器学习】决策树算法--2(代码模块实现)_第7张图片

3.2、注意事项

添加下方红圈里代码防止乱码(无线显示中文)。
【机器学习】决策树算法--2(代码模块实现)_第8张图片

列表索引必须是整数或切片,而不是元组,而下方又是以是否元组为判断连续值的依据,所以直接改最好特征标签,同时也不影响离散型操作。
【机器学习】决策树算法--2(代码模块实现)_第9张图片
【机器学习】决策树算法--2(代码模块实现)_第10张图片

4、预剪枝介绍及实现

预剪枝 , 即在生成决策树的过程中提前停止树的增长。核心思想是在树中结点进行扩展之前,先计算当前的划分是否能带来模型泛化能力的提升,如果不能,则不再继续生长子树。此时可能存在不同类别的样本同时存于结点中,按照多数投票的原则判断该结点所属类别。预剪枝对于何时停止决策树的生长有以下几种方法:
( 1 )当树到达一定深度的时候,停止树的生长。
( 2 )当到达当前结点的样本数量小于某个阈值的时候,停止树的生长。
( 3 )计算每次分裂对测试集的准确度提升,当小于某个阈值的时候 ,不再继续扩展。

集美大学三好学生评选表

是否挂科 获得奖学金次数 综测评价 体质健康是否达标 宿舍检评 是否符合条件
1 no 4 excellect yes excellent yes
3 no 0 excellect yes excellent yes
7 no 1 excellect yes excellent yes
5 no 2 good yes excellent yes
4 no 1 excellect no excellent no
2 no 1 good no excellent no
6 no 1 excellect yes excellent no
8 yes 0 good yes excellent no
9 no 2 good yes good no
10 no 2 excellect yes excellent yes
14 no 4 excellect yes excellent yes
15 no 2 excellect yes excellent yes
11 yes 2 excellect yes excellent no
12 yes 0 good yes good no
13 yes 0 excellect yes pass no

还是以集美大学三好学生评选表为例:
将1-9作为训练集,10-15作为验证集
未剪枝之前的图是这样:

【机器学习】决策树算法--2(代码模块实现)_第11张图片

开始对Failclass进行验证,再划分前:
在这里插入图片描述

划分后(验证过程:首先划分前对验证集可以看出3yes,3no所以验证集精度为3 / 6 = 50%,对其进行划分得到no,yes两者,在训练集里yes对应的标签都为no,no对应的标签4yes,4no,所以就以yes为标签,在转移到验证集里no对应的标签都是yes,所以{10,14,15}还是三个,3 / 6 = 50%,结果还是50%没得到提升,那就不划分了):
在这里插入图片描述
最后得到的结果是:

【机器学习】决策树算法--2(代码模块实现)_第12张图片
但是,如果你继续往下分会发现:
【机器学习】决策树算法--2(代码模块实现)_第13张图片

然后继续对这个Scholarship-num划分:得到0对应标签是yes,1对应标签是no,2对应标签是yes,4对应标签是yes,所以到验证里,0,2,4对应标签都是yes,{10,14,15,11,12,13}六个全是,6 / 6 = 100%,精度提高了所以,那不是应该要继续分吗,但是之前又不划分,这不就矛盾了吗?
【机器学习】决策树算法--2(代码模块实现)_第14张图片

预剪枝思考:

显然,根据上图可知预剪枝使得很多分支没有展开,虽然这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间。但是,有些分支虽当前不能提升泛化性。甚至可能导致泛化性暂时降低,但在其基础上进行后续划分却有可能导致显著提高,因此预剪枝的这种贪心本质,给决策树带来了欠拟合的风险。所以,接下来带来后剪枝方法

5、后剪枝介绍及实现

后剪枝,是在已经生成的过拟合决策树上进行剪枝,得到简化版的剪枝决策树。核心思想是让算法生成一棵完全生长的决策树,然后从最底层向上计算是否剪枝。剪枝过程将子树删除,用一个叶子结点替代,该结点的类别同样按照多数投票的原则进行判断。 同样地 ,后剪枝也可以通过在测试集上的准确率进行判断,如果剪枝过后准确率有所提升,则进行剪枝。 相比于预剪枝,后剪枝方法通常可以得到泛化能力更强的决策树,但时间开销会更大。
还是以上面的集美大学三好学生评选表为例:
未剪枝结果如下:

【机器学习】决策树算法--2(代码模块实现)_第15张图片

未剪枝前还是之前算的50%,自底向上,对Hostel-assessment进行剪枝excellent对应的标签是yes,good对应的标签是no,转到验证集{10,14,15,11}是yes,那么4 / 6 = 66.7%是划分后验证集精度,那么看Hostel-assessment在Physically-fit是yes的范围里看yes多还是no多,所以可代替为yes,那么则变为

【机器学习】决策树算法--2(代码模块实现)_第16张图片
继续对Physically-fit划分,此时验证集精度为如图:
【机器学习】决策树算法--2(代码模块实现)_第17张图片
Physically-fit在训练集里yes对应的标签为4yes,3no,则为yes,no对应的标签全为no,则为no,所以{10,14,15,11,12,13}全为yes,则6 / 6 = 100%验证集精度提升,那么就将此节点代替为no(在Scholarship-num=1下no标签大于1)所以变成如下图:
【机器学习】决策树算法--2(代码模块实现)_第18张图片

然后Hostel-assessment划分后的验证集精度是66.7%,也是没有提升,跟全部树刚开始的精度一样没提升所以,最后剪枝后的树如下图:

【机器学习】决策树算法--2(代码模块实现)_第19张图片

后剪枝相比于预剪枝:

显然,从上图可知后剪枝决策树通常比预剪枝决策树保留了更多的分支。一般情况下,后剪枝决策树的欠拟合风险很小,泛化性能往往由于预剪枝决策树,但是后剪枝过程是在生成完全决策树后进行的,并且要自下往上地对树中的非叶子节点逐一进行考察计算,因此训练时间的开销比为剪枝和预剪枝决策树都要大得多。

预剪枝实现:

# 创建预剪枝决策树
def createTreePrePruning(dataTrain, labelTrain, dataTest, labelTest, names):
   
    trainData = np.asarray(dataTrain)
    labelTrain = np.asarray(labelTrain)
    testData = np.asarray(dataTest)
    labelTest = np.asarray(labelTest)
    names = np.asarray(names)
    # 如果结果为单一结果
    if len(set(labelTrain)) == 1: 
        return labelTrain[0] 
    # 如果没有待分类特征
    elif trainData.size == 0: 
        return majorityCnt(labelTrain)
    # 其他情况则选取特征 
    bestFeat, bestEnt = chooseBestFeatureToSplit(dataTrain, labelTrain)
    # 取特征名称
    bestFeatName = names[bestFeat]
    # 从特征名称列表删除已取得特征名称
    names = np.delete(names, [bestFeat])
    # 根据最优特征进行分割
    dataTrainSet, labelTrainSet = splitFeatureData(dataTrain, labelTrain, bestFeat)

    # 预剪枝评估
    # 划分前的分类标签
    labelTrainLabelPre = majorityCnt(labelTrain)
    labelTrainRatioPre = equalNums(labelTrain, labelTrainLabelPre) / labelTrain.size
    # 划分后的精度计算 
    if dataTest is not None: 
        dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, bestFeat)
        # 划分前的测试标签正确比例
        labelTestRatioPre = equalNums(labelTest, labelTrainLabelPre) / labelTest.size
        # 划分后 每个特征值的分类标签正确的数量
        labelTrainEqNumPost = 0
        for val in labelTrainSet.keys():
            labelTrainEqNumPost += equalNums(labelTestSet.get(val), majorityCnt(labelTrainSet.get(val))) + 0.0
        # 划分后 正确的比例
        labelTestRatioPost = labelTrainEqNumPost / labelTest.size 
    
    # 如果没有评估数据 但划分前的精度等于最小值0.5 则继续划分
    if dataTest is None and labelTrainRatioPre == 0.5:
        decisionTree = {bestFeatName: {}}
        for featValue in dataTrainSet.keys():
            decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue), labelTrainSet.get(featValue)
                                      , None, None, names)
    elif dataTest is None:
        return labelTrainLabelPre 
    # 如果划分后的精度相比划分前的精度下降, 则直接作为叶子节点返回
    elif labelTestRatioPost < labelTestRatioPre:
        return labelTrainLabelPre
    else :
        # 根据选取的特征名称创建树节点
        decisionTree = {bestFeatName: {}}
        # 对最优特征的每个特征值所分的数据子集进行计算
        for featValue in dataTrainSet.keys():
            decisionTree[bestFeatName][featValue] = createTreePrePruning(dataTrainSet.get(featValue), labelTrainSet.get(featValue)
                                      , dataTestSet.get(featValue), 		labelTestSet.get(featValue)
                                      , names)
    return decisionTree 


xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest = splitXgData20(xgData, xgLabel)
# 生成不剪枝的树
xgTreeTrain = createTree(xgDataTrain, xgLabelTrain, xgName)
# 生成预剪枝的树
xgTreePrePruning = createTreePrePruning(xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest, xgName)
# 画剪枝前的树
print("剪枝前的树")
createPlot(xgTreeTrain)
# 画剪枝后的树
print("剪枝后的树")
createPlot(xgTreePrePruning)

对数据集进行分类前9个为训练集,后6个为验证集

def splitXgData20(xgData, xgLabel):
    xgDataTrain = xgData[[0, 1, 2, 3, 4, 5, 6, 7, 8],:]
    xgDataTest = xgData[[9, 10, 11, 12, 13, 14],:]
    xgLabelTrain = xgLabel[[0, 1, 2, 3, 4, 5, 6, 7, 8]]
    xgLabelTest = xgLabel[[9, 10, 11, 12, 13, 14]]
    return xgDataTrain, xgLabelTrain, xgDataTest, xgLabelTest

运行结果:
【机器学习】决策树算法--2(代码模块实现)_第20张图片

后剪枝实现

# 后剪枝 训练完成后决策节点进行替换评估  这里可以直接对xgTreeTrain进行操作
def treePostPruning(labeledTree, dataTest, labelTest, names):
    newTree = labeledTree.copy()
    dataTest = np.asarray(dataTest)
    labelTest = np.asarray(labelTest)
    names = np.asarray(names)
    # 取决策节点的名称 即特征的名称
    featName = list(labeledTree.keys())[0]
    print("\n当前节点:" + featName)
    # 取特征的列
    featCol = np.argwhere(names==featName)[0][0]
    names = np.delete(names, [featCol])
    print("当前节点划分的数据维度:" + str(names))
    print("当前节点划分的数据:" )
    print(dataTest)
    print(labelTest)
    # 该特征下所有值的字典
    newTree[featName] = labeledTree[featName].copy()
    featValueDict = newTree[featName]
    featPreLabel = featValueDict.pop("_vpdl")
    print("当前节点预划分标签:" + featPreLabel)
    # 是否为子树的标记
    subTreeFlag = 0
    # 分割测试数据 如果有数据 则进行测试或递归调用  np的array我不知道怎么判断是否None, 用is None是错的
    dataFlag = 1 if sum(dataTest.shape) > 0 else 0
    if dataFlag == 1:
        print("当前节点有划分数据!")
        dataTestSet, labelTestSet = splitFeatureData(dataTest, labelTest, featCol)
    for featValue in featValueDict.keys():
        print("当前节点属性 {0} 的子节点:{1}".format(featValue ,str(featValueDict[featValue])))
        if dataFlag == 1 and type(featValueDict[featValue]) == dict:
            subTreeFlag = 1 
            # 如果是子树则递归
            newTree[featName][featValue] = treePostPruning(featValueDict[featValue], dataTestSet.get(featValue), labelTestSet.get(featValue), names)
            # 如果递归后为叶子 则后续进行评估
            if type(featValueDict[featValue]) != dict:
                subTreeFlag = 0 
            
        # 如果没有数据  则转换子树
        if dataFlag == 0 and type(featValueDict[featValue]) == dict: 
            subTreeFlag = 1 
            print("当前节点无划分数据!直接转换树:"+str(featValueDict[featValue]))
            newTree[featName][featValue] = convertTree(featValueDict[featValue])
            print("转换结果:" + str(convertTree(featValueDict[featValue])))
    # 如果全为叶子节点, 评估需要划分前的标签,这里思考两种方法,
    #     一是,不改变原来的训练函数,评估时使用训练数据对划分前的节点标签重新打标
    #     二是,改进训练函数,在训练的同时为每个节点增加划分前的标签,这样可以保证评估时只使用测试数据,避免再次使用大量的训练数据
    #     这里考虑第二种方法 写新的函数 createTreeWithLabel,当然也可以修改createTree来添加参数实现
    if subTreeFlag == 0:
        ratioPreDivision = equalNums(labelTest, featPreLabel) / labelTest.size
        equalNum = 0
        for val in labelTestSet.keys():
            equalNum += equalNums(labelTestSet[val], featValueDict[val])
        ratioAfterDivision = equalNum / labelTest.size 
        print("当前节点预划分标签的准确率:" + str(ratioPreDivision))
        print("当前节点划分后的准确率:" + str(ratioAfterDivision))
        # 如果划分后的测试数据准确率低于划分前的,则划分无效,进行剪枝,即使节点等于预划分标签
        # 注意这里取的是小于,如果有需要 也可以取 小于等于
        if ratioAfterDivision < ratioPreDivision:
            newTree = featPreLabel 
    return newTree



xgTreeBeforePostPruning = {'Failclass': {"_vpdl": "是"
                                        ,'yes': 'no', 'no': {'Scholarship-num': {"_vpdl": "是"
                                        ,'0': 'yes', '1': {'Physically-fit': {"_vpdl": "是",'yes': {'Hostel-assessment': {"_vpdl": "是"
                                        ,'excllent': {'Grade-ranking': {"_vpdl": "是",'excllent': 'yes'}}}}, 'no': 'no'}}, '2': {'Hostel-assessment': {'good': 'no', 'excllent': 'yes'}}, '4': 'yes'}}}}
                                        

xgTreePostPruning = treePostPruning(xgTreeBeforePostPruning, xgDataTest, xgLabelTest, xgName)
createPlot(convertTree(xgTreeBeforePostPruning))
createPlot(xgTreeBeforePostPruning)


后剪枝评估时需要划分前的标签,这里思考两种方法:
一是,不改变原来的训练函数,评估时使用训练数据对划分前的节点标签重新打标
二是,改进训练函数,在训练的同时为每个节点增加划分前的标签,这样可以保证评估时只使用测试数据,避免再次使用大量的训练数据

结果如图:
【机器学习】决策树算法--2(代码模块实现)_第21张图片
【机器学习】决策树算法--2(代码模块实现)_第22张图片
代码参考及后剪枝划分标签参考:https://blog.csdn.net/ylhlly/articl****e/details/93213633

6、总结

**上述决策树算法用的时ID3算法,接之前的【机器学习】决策树算法–1(算法介绍)中ID3算法已经以上述例子作了具体的讲述,具体算法核心过程:从根节点开始,计算所有特征里的信息熵,之后算出信息增益,并选择信息增益最大的特征作为下一个节点,继续建立这个特征的子节点,在对子节点进行递归调用上述方法,构建决策树,直到所有特征遍历完或者其信息增益均很小时停止;但是,对于编号的特征属性还是比其他特征属性要有所喜好。所以,信息增益对可取值数目较多的属性有所喜好。
还有就是离散型和连续型数据的区别,学会理解两种类型的区别以及决策树中处理方法。
对比未剪枝的决策树和经过预剪枝的决策树可以看出:预剪枝使得决策树的很多分支都没有“展开”,这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间开销。但是,另一方面,因为预剪枝是基于“贪心”的,所以,虽然当前划分不能提升泛华性能,但是基于该划分的后续划分却有可能导致性能提升,因此预剪枝决策树有可能带来欠拟合的风险。

**

你可能感兴趣的:(决策树,算法)