决策树算法(下)

大部分内容转载自 https://blog.csdn.net/herosofearth/article/details/52425952

前言

上篇博文已经介绍了ID3、C4.5生成决策树的算法。由于上文使用的测试数据以及建立的模型都比较简单,所以其泛化能力很好。但是,当训练数据量很大的时候,建立的决策树模型往往非常复杂,树的深度很大。此时虽然对训练数据拟合得很好,但是其泛化能力即预测新数据的能力并不一定很好,也就是出现了过拟合现象。这个时候我们就需要对决策树进行剪枝处理以简化模型。另外,CART算法也可用于建立回归树。本文先承接上文介绍完整分类决策树,再简单介绍回归树。

三、CART算法

算法概述

CART(Classification And Regression Tree)算法是一种决策树分类方法。

它采用一种二分递归分割的技术,分割方法采用基于最小距离的基尼指数估计函数,将当前的样本集分为两个子样本集,使得生成的的每个非叶子节点都有两个分支。因此,CART算法生成的决策树是结构简洁的二叉树。

分类树

如果目标变量是离散变量,则是classfication Tree。
分类树是使用树结构算法将数据分成离散类的方法。

回归树

如果目标是连续变量,则是Regression Tree。

CART树是二叉树,不像多叉树那样形成过多的数据碎片。

分类树两个关键点

(1)将训练样本进行递归地划分自变量空间进行建树

(2)用验证数据进行剪枝。

a.对于离散变量X(x1…xn)

分别取X变量各值的不同组合,将其分到树的左枝或右枝,并对不同组合而产生的树,进行评判,找出最佳组合。如果只有两个取值,好办,直接根据这两个值就可以划分树。取值多于两个的情况就复杂一些了,如变量年纪,其值有“少年”、“中年”、“老年”,则分别生产{少年,中年}和{老年},{上年、老年}和{中年},{中年,老年}和{少年},这三种组合,最后评判对目标区分最佳的组合。因为CART二分的特性,当训练数据具有两个以上的类别,CART需考虑将目标类别合并成两个超类别,这个过程称为双化。这里可以说一个公式,n个属性,可以分出(2^n-2)/2种情况。

b.对于连续变量X(x1…xn)

首先将值排序,分别取其两相邻值的平均值点作为分隔点,将树一分成左枝和右枝,不断扫描,进而判断最佳分割点。特征值大于分裂值就走左子树,或者就走右子树。

这里有一个问题,这次选中的分裂属性在下次还可以被选择吗?对于离散变量XD,如果XD只有两种取值,那么在这一次分裂中,根据XD分裂后,左子树中的subDataset中每个数据的XD属性一样,右子树中的subDataset中每个数据的XD属性也一样,所以在这个节点以后,XD都不起作用了,就不用考虑XD了。XD取3种,4种。。。的情况大家自己想想,不难想明白。至于连续变量XC,离散化后相当于一个可以取n个值的离散变量,按刚刚离散变量的情况分析。除非XC的取值都一样,否则这次用了XC作为分裂属性,下次还要考虑XC。

变量和最佳切分点选择原则:

树的生长,总的原则是,让枝比树更纯,而度量原则是根据不纯对指标来衡量,对于分类树,则用GINI指标、Twoing指标、Order Twoing等;如果是回归树则用,最小平方残差、最小绝对残差等指标衡量

(1)GINI指标(Gini越小,数据越纯)——针对离散目标
这里写图片描述
这里写图片描述
这里写图片描述

(2)最小平方残差——针对连续目标
这里写图片描述
其思想是,让组内方差最小,对应组间方差最大,这样两组,也即树分裂的左枝和右枝差异化最大。
通过以上不纯度指标,分别计算每个变量的各种切分/组合情况,找出该变量的最佳值组合/切分点;再比较各个变量的最佳值组合/切分点,最终找出最佳变量和该变量的最佳值组合/切分点

CART,即分类与回归树(classification and regression tree),也是一种应用很广泛的决策树学习方法。但是CART算法比较强大,既可用作分类树,也可以用作回归树。作为分类树时,其本质与ID3、C4.5并有多大区别,只是选择特征的依据不同而已。另外,CART算法建立的决策树一般是二叉树,即特征值只有yes or no的情况(个人认为并不是绝对的,只是看实际需要)。当CART用作回归树时,以最小平方误差作为划分样本的依据。

1.分类树
(1)基尼指数

分类树采用基尼指数选择最优特征。假设有 K K 个类,样本点属于第k类的概率为 pk p k ,则概率分布的基尼指数定义为:

Ginip(p)=Kk=1pk(1pk)=1Kk=1p2k G i n i p ( p ) = ∑ k = 1 K p k ( 1 − p k ) = 1 − ∑ k = 1 K p k 2

对于二分类问题,若样本属于第一类的概率为 p p ,则概率分布的基尼指数为

Ginip(p)=p(1p)

对于给定的样本集合D,其基尼指数为

Ginip(D)=1Kk=1(|Ck||D|)2 G i n i p ( D ) = 1 − ∑ k = 1 K ( | C k | | D | ) 2

这里, Ck C k D D 中属于第k类的样本子集, K K 是类的个数。
Python计算如下:

def calcGini(dataSet):
    '''
            计算基尼指数
    :param dataSet:数据集
    :return: 计算结果
    '''
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet: # 遍历每个实例,统计标签的频数
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys(): 
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    Gini = 1.0
    for key in labelCounts:
        prob = float(labelCounts[key]) / numEntries
        Gini -= prob * prob # 以2为底的对数
    return Gini

AD

Ginip(D,A)=|D1|DGinip(D1)+|D2|DGinip(D2) G i n i p ( D , A ) = | D 1 | D G i n i p ( D 1 ) + | D 2 | D G i n i p ( D 2 )

Gini(D)D 基 尼 指 数 G i n i ( D ) 表 示 集 合 D 的不确定性,基尼指数 Gini(D,A) G i n i ( D , A ) 表示经 A=a A = a 分割后集合D的不确定性。基尼指数值越大,样本集合的不确定性也就越大,这一点与熵相似。
Python计算如下:

def calcGiniWithFeat(dataSet, feature, value):
    '''
            计算给定特征下的基尼指数
    :param dataSet:数据集
    :param feature:特征维度
    :param value:该特征变量所取的值
    :return: 计算结果
    '''
    D0 = []; D1 = []
    # 根据特征划分数据
    for featVec in dataSet:
        if featVec[feature] == value:
            D0.append(featVec)
        else:
            D1.append(featVec)
    Gini = len(D0) / len(dataSet) * calcGini(D0) + len(D1) / len(dataSet) * calcGini(D1)
    return Gini
(2)CART分类树的算法步骤如下:

1

Python实现如下:

def chooseBestSplit(dataSet):
    numFeatures = len(dataSet[0])-1
    bestGini = inf; bestFeat = 0; bestValue = 0; newGini = 0
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)
        for splitVal in uniqueVals:
            newGini = calcGiniWithFeat(dataSet, i, splitVal)
            if newGini < bestGini:
                bestFeat = i
                bestGini = newGini
    return bestFeat
#     for featVec in dataSet:
#         for splitVal in set(dataSet[:,featIndex].tolist()):
#             newGini = calcGiniWithFeat(dataSet, featIndex, splitVal)
#             if newGini < bestGini: 
#                 bestFeat = featIndex
#                 bestValue = splitVal
#                 bestGini = newGini                


def majorityCnt(classList):
    '''
           采用多数表决的方法决定叶结点的分类
    :param: 所有的类标签列表
    :return: 出现次数最多的类
    '''
    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:训练数据集
    :return: 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 = chooseBestSplit(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)
    return myTree

代码结构跟上篇博文是基本一样的,不同的只有选择特征的方式,我们导入数据测试一下:

if __name__ == "__main__":
    dataSet,labels = createDataSet()
    subLabels = labels[:]
    myTree = createTree(dataSet, labels)
    print(myTree)
    treePlotter.createPlot(myTree)

2

见这棵决策树是非常复杂的。我们可以测试一下它的泛化能力。计算预测误差的代码如下:

testData,testLabels = loadTestData()
testErr = calcTestErr(myTree, testData, subLabels)

2
测试数据集中有6组样本。由结果可知,有一组样本预测不正确,那么预测误差率为16.7%左右。实际上这个模型并不是很好用的,尤其是在数据量更大的预测集中。此时我们需要简化这棵决策树,防止过拟合现象。

2.剪枝(pruning)

在决策树学习中将已生成的树进行简化的过程称为剪枝。决策树的剪枝往往通过极小化决策树的损失函数或代价函数来实现。实际上剪枝的过程就是一个动态规划的过程:从叶结点开始,自底向上地对内部结点计算预测误差以及剪枝后的预测误差,如果两者的预测误差是相等或者剪枝后预测误差更小,当然是剪掉的好。但是如果剪枝后的预测误差更大,那就不要剪了。剪枝后,原内部结点会变成新的叶结点,其决策类别由多数表决法决定。不断重复这个过程往上剪枝,直到预测误差最小为止。剪枝的实现代码如下:

# 计算预测误差 
def calcTestErr(myTree,testData,labels):
    errorCount = 0.0
    for i in range(len(testData)): 
        if classify(myTree,labels,testData[i]) != testData[i][-1]:
            errorCount += 1 
    return float(errorCount)

# 计算剪枝后的预测误差
def testMajor(major,testData):  
    errorCount = 0.0  
    for i in range(len(testData)):  
        if major != testData[i][-1]:  
            errorCount += 1   
    return float(errorCount)

def pruningTree(inputTree,dataSet,testData,labels):  
    firstStr = list(inputTree.keys())[0]  
    secondDict = inputTree[firstStr]        # 获取子树
    classList = [example[-1] for example in dataSet]  
    featKey = copy.deepcopy(firstStr)  
    labelIndex = labels.index(featKey)  
    subLabels = copy.deepcopy(labels)
    del(labels[labelIndex])  
    for key in list(secondDict.keys()):  
        if isTree(secondDict[key]):
            # 深度优先搜索,递归剪枝
            subDataSet = splitDataSet(dataSet,labelIndex,key)
            subTestSet = splitDataSet(testData,labelIndex,key)
            if len(subDataSet) > 0 and len(subTestSet) > 0:
                inputTree[firstStr][key] = pruningTree(secondDict[key],subDataSet,subTestSet,copy.deepcopy(labels))
    if calcTestErr(inputTree,testData,subLabels) < testMajor(majorityCnt(classList),testData):
        # 剪枝后的误差反而变大,不作处理,直接返回
        return inputTree 
    else:
        # 剪枝,原父结点变成子结点,其类别由多数表决法决定
        return majorityCnt(classList)

3

真的是简单得太多了。看看它的泛化能力:
4

3.回归树

回归树的生成实际上也是贪心算法。与分类树不同的是回归树处理的数据连续分布的。废话不多说了,直接贴算法:
这里写图片描述

CART回归树算法划分样本的依据是最小平方误差。Python实现如下:

# 生成叶结点
def regLeaf(dataSet):
    return mean(dataSet[:,-1])
# 计算平方误差
def regErr(dataSet):
    return var(dataSet[:,-1]) * shape(dataSet)[0]

def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
    tolS = ops[0]; tolN = ops[1]
    if len(set(dataSet[:,-1].T.tolist())) == 1: # 停止条件:样本属于同一个类
        return None, leafType(dataSet)
    m,n = shape(dataSet)
    S = errType(dataSet)
    bestS = inf; bestIndex = 0; bestValue = 0
    for featIndex in range(n-1):
        for splitVal in set(dataSet[:,featIndex].tolist()):# 固定特征,并为每个特征选择最优二分特征值
            R0, R1 = binSplitDataSet(dataSet, featIndex, splitVal)
            if (shape(R0)[0] < tolN) or (shape(R1)[0] < tolN): continue
            newS = errType(R0) + errType(R1)
            if newS < bestS: 
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    # 如果误差下降值小于一个阈值,则不要划分
    if (S - bestS) < tolS: 
        return None, leafType(dataSet) #exit cond 2
    R0, R1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    if (shape(R0)[0] < tolN) or (shape(R1)[0] < tolN):  # 停止条件:样本数小于一个阈值
        return None, leafType(dataSet)
    return bestIndex,bestValue

构建回归树如下:

def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):#assume dataSet is NumPy Mat so we can array filtering
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)# 选择最优二分方式
    if feat == None: return val    
    retTree = {}
    retTree['spInd'] = feat
    retTree['spVal'] = val
    leftSet, rightSet = binSplitDataSet(dataSet, feat, val)
    retTree['left'] = createTree(leftSet, leafType, errType, ops)
    retTree['right'] = createTree(rightSet, leafType, errType, ops)
    return retTree

回归树同样有一个剪枝过程:

def isTree(obj):
    return (type(obj).__name__=='dict')

def getMean(tree):
    if isTree(tree['right']): tree['right'] = getMean(tree['right'])
    if isTree(tree['left']): tree['left'] = getMean(tree['left'])
    return (tree['left']+tree['right'])/2.0

def prune(tree, testData):
    if shape(testData)[0] == 0: return getMean(tree) # 如果没有测试数据则对树进行塌陷处理
    if (isTree(tree['right']) or isTree(tree['left'])):
        lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
    # 深度优先搜索
    if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet)
    if isTree(tree['right']): tree['right'] =  prune(tree['right'], rSet)
    # 到达叶结点
    if not isTree(tree['left']) and not isTree(tree['right']):
        lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
        # 未剪枝的误差
        errorNoMerge = sum(power(lSet[:,-1] - tree['left'],2)) +\
            sum(power(rSet[:,-1] - tree['right'],2))
        treeMean = (tree['left']+tree['right'])/2.0
        # 剪枝后的误差
        errorMerge = sum(power(testData[:,-1] - treeMean,2))
        if errorMerge < errorNoMerge: 
            print("merging")
            return treeMean
        else: return tree
    else: return tree

相比线性回归,回归树可以对非线性数据建立模型。这个算法可以使用任意一个测试线性回归的数据集来测试,这里就不再演示了。

五、总结

总体来讲,决策树模型是一个比较容易理解模型。它建立起来的模型直观、形象,也比较贴近人们的思维习惯。决策树更多地用于分类问题而不是回归问题。通常,在使用更复杂的算法之前,一般先建议使用决策树,并将它的准确率作为性能基准。另外,决策树还可以帮助我们提取重要特征。作为机器学习十大算法之一,决策树有着它相当重要的地位,基本上市面上能见到的机器学习书籍必定会讲这个算法。然而,决策树的研究并不止于此。关于决策树更深的模型有软决策树、决策森林、随机森林等。

分类树测试数据(包含训练集和测试集):http://download.csdn.net/detail/herosofearth/9621052

对于例5.1,应用CART算法生成决策树
这里写图片描述

这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值