• 《机器学习实战》学习笔记第九章 —— 决策树之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,预测的效果越好。

    首先是回归树:

    然后是模型树:

    最后是线性回归:

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

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

     

     

     

    
    
  • 相关阅读:
    UI测试
    软件测试用例详解(转载)
    Mac设置命令别名
    CentOS7 开启免密登陆
    使用systemctl命令管理服务mysql
    Redis学习笔记02--主从数据库配置
    CentOS使用dnf安装Redis
    CentOS 7 防火墙设置
    Redis学习笔记01---配置文件
    CentOS7搭建Maven的Nexus私服仓库
  • 原文地址:https://www.cnblogs.com/DOLFAMINGO/p/9498612.html
Copyright © 2020-2023  润新知