《机器学习实战》学习笔记第九章 —— 决策树之CART算法

 

 

相关博文:

《机器学习实战》学习笔记第三章 —— 决策树

 

 

主要内容:

一.CART算法简介

二.分类树

三.回归树

四.构建回归树

五.回归树的剪枝

六.模型树

七.树回归与标准回归的比较

 

 

一.CART算法简介

1.对于上一篇博客所介绍的决策树,其使用的算法是ID3算法或者是C4.5算法,他们都是根据特征的所有取值情况来进行分割结点的。也正因如此,这两种算法都只能用于离散型的特征,而不能处理连续型的特征。为了解决这个问题,我们使用二元切分法来对连续型的特征进行处理。所谓二元切分法,其实就是一个对特征进行 True or False的判断(最简单如:是或不是、小于或大于等于),这个判断就将数据分割成两半,而不管其特征是连续型的还是离散型的。显而易见,以这种方法构建出来的决策树是一棵二叉树。这就是CART算法最基本的思路。

 

 

二.分类树

1.CART算法使用在离散型特征的数据上,则称为分类树(分类还是回归不是以Y来界定的吗?为什么这里以特征X来界定? CART算法使用在离散型标签的数据上,称为分类树。在这里不再像ID3算法那样使用熵来衡量数据(指的是Y)的不确定性,而是使用“基尼指数”。基尼指数的详情如下:

 

2.CART算法之分类树:

(为什么选择基尼系数最小的?基尼系数与熵类似,其值越大,不确定性越大,那么选择分割后基尼系数最小的,表明不确定性越小了,类别就越能确定了) 

 

三.回归树

1.同样地,在CART算法之回归树中,数据的不确定性不再是用熵来衡量,但也不是用基尼指数,而是用总方差

问1)为什么分类树中不用熵,而是用基尼指数呢?

这个问题倒真的想不到解释,感觉两种都可以用来衡量离散型变量的不确定性。待解决……

问2)为什么回归树中不用熵或者基尼指数呢?

这个很容易解释,离散型变量可以数出现的个数来计算概率,但是连续型变量,对于单单一个点的值而言,是没有概率的,所以熵或者基尼指数不能用来衡量连续型变量的不确定性。

问3)为什么使用总方差而不是方差(总方差/m)呢?

《统计学习方法》里面提到一句话“基尼指数值越大,样本集合的不确定性也越大”,其中个人觉得“样本集合”这个词是关键。所谓样本集合,一个表现特征就是规模。我想,规模也是影响“数据混乱程度”的一个因素。所以使用总方差。当然这只是个人感性的理解,并没有任何的理论参考或推敲。

 

2.CART算法之回归树:

 

 

四.构建回归树

1.算法流程:

 

2.代码及注释:

def binSplitDataSet(dataSet, feature, value):   #根据分割特征及其值将数据分成两半
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
    return mat0,mat1

'''选择最好的分割特征及其值'''
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
    tolS = ops[0]; tolN = ops[1]  #tolS是分割误差减少的下限,tolN是分割后每个子树的结点个数下限
    if len(set(dataSet[:,-1].T.tolist()[0])) == 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]):  #枚举该特征下在训练数据中所有出现的值,所谓分割线
            mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)  #将数据集切割成两半
            if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): continue  #如果切割后某一子树的结点数少于下限,则此次分割无效
            newS = errType(mat0) + errType(mat1)
            if newS < bestS:        #更新最小总方差下的分割特征及其值
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    if (S - bestS) < tolS:      #如果在最好的情况下(即总方差减少得最多),总方差的减少量仍然少于下限,则此次分割无效,直接返回当前数据集作为叶子结点
        return None, leafType(dataSet) #exit cond 2
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):  #话说这一步不是已经在双重循环里面了吗?
        return None, leafType(dataSet)
    return bestIndex,bestValue

def regLeaf(dataSet):   #生成叶子结点,均值作为返回值(即预测值)
    return mean(dataSet[:,-1])

def regErr(dataSet):    #计算总方差
    return var(dataSet[:,-1]) * shape(dataSet)[0]

'''构建回归树:leafType是建立叶子结点的函数,errType是计算误差的函数,ops是参数元组(用于预剪枝)'''
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 #if the splitting hit a stop condition return val
    retTree = {}
    retTree['spInd'] = feat     #记录特征
    retTree['spVal'] = val          #记录值
    lSet, rSet = binSplitDataSet(dataSet, feat, val)    #将数据集分割成两部分,然后递归左右子树继续生产回归树
    retTree['left'] = createTree(lSet, leafType, errType, ops)
    retTree['right'] = createTree(rSet, leafType, errType, ops)
    return retTree

 

 

五.回归树的剪枝

1.剪枝有预剪枝和后剪枝两种。预剪枝对设定的参数非常敏感,如上面代码中tolS和tolN两个参数,分别是分割误差减少的下限、分割后每个子树的结点个数下限。基于预剪枝的性能不太好控制,我们就应着手于后剪枝的研究,其伪代码如下:

2.代码及注释如下:

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

 

 

六.模型树

1.上面所介绍的决策树中,所有叶子结点,也就是预测值都是直接设定为在该叶子节点上的数据的Y的均值。简而言之,就是叶子结点放的是均值,是一个确定的值。但除此之外,我们还可以在叶子节点上放一个函数,以此进行预测

2.例如,我们可以在叶子结点上放一个线性回归模型,也正因如此,前面代码中生成叶子结点的方式以及计算总方差的方式方式了改变。详情如下:

'''线性回归模型'''
def linearSolve(dataSet):   #利用最小二乘法计算线性回归模型的参数ws
    m,n = shape(dataSet)
    X = mat(ones((m,n))); Y = mat(ones((m,1))) #create a copy of data with 1 in 0th postion
    X[:,1:n] = dataSet[:,0:n-1]; Y = dataSet[:,-1]#and strip out Y
    xTx = X.T*X
    if linalg.det(xTx) == 0.0:
        raise NameError('This matrix is singular, cannot do inverse,

        try increasing the second value of ops')
    ws = xTx.I * (X.T * Y)
    return ws,X,Y

'''叶子结点为线性回归函数'''
def modelLeaf(dataSet):#返回线性回归模型的参数ws
    ws,X,Y = linearSolve(dataSet)
    return ws

def modelErr(dataSet):      #计算总方差,因而Y值是X的函数,而不再是之前的均值,所以不能利用以前的误差计算方法
    ws,X,Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(power(Y - yHat,2))

 

 

七.回归树、模型树、线性回归的比较

既然介绍了回归树与模型树,且模型树又用到了线性回归模型,且三者都能对同样的数据进行预测,那就理所当然地对它们作出一些比较,分出优劣。具体实现实现如下:

 '''普通回归树叶子结点返回均值。为什么要这个没有用的inData?其实只是为了方便统一输入参数,因为模型树需要输入参数'''
def regTreeEval(model,inData ):
    return float(model)

def modelTreeEval(model, inDat):        #模型树叶子结点对测试数据的预测值, 与上面的regTreeEval()函数是同类型
    n = shape(inDat)[1]
    X = mat(ones((1,n+1)))
    X[:,1:n+1]=inDat
    return float(X*model)

'''modelEval是计算叶子结点的值的函数,可以是regLeaf()对应普通回归树,可以是modelTreeEval()对应模型树'''
def treeForeCast(tree, inData, modelEval=regTreeEval):      #搜索回归树,找到合适的预测值.
    if not isTree(tree): return modelEval(tree, inData)
    if inData[tree['spInd']] > tree['spVal']:
        return treeForeCast(tree['left'], inData, modelEval)
    else:
        return treeForeCast(tree['right'], inData, modelEval)

def createForeCast(tree, testData, modelEval=regTreeEval):      #对测试数据集进行预测
    m=len(testData)
    yHat = mat(zeros((m,1)))
    for i in range(m):      #枚举每一个数据,并对其进行预测
        yHat[i,0] = treeForeCast(tree, mat(testData[i]), modelEval)
    return yHat

 

然后测试一下三者对测试数据的预测效果,这里用相关系数R2来衡量,R2的值越接近于1.0,预测的效果越好。

首先是回归树:

然后是模型树:

最后是线性回归:

从上面可以看得出:模型树 > 回归树 > 线性回归 。

所以多做了点功夫,效果就较之好一点,是说得过去的。

 

 

 


原文地址:https://www.cnblogs.com/DOLFAMINGO/p/9498612.html