《机器学习实战笔记--第二部分 利用回归预测数值型数据:树回归1》

    当数据拥有众多特征且特征之间的关系十分复杂时,构建全局的模型就太难了。实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。

def loadDataSet(fileName):
    fr = open(fileName)
    dataMat = []
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = map(float, curLine)
        dataMat.append(fltLine)
    return dataMat

def binSplitDataSet(dataSet, feature, value):
    '''数据集,待切分的特征,该特征的某个值'''
    # np.nonzero函数用于得到数组array中非零元素的位置(数组索引)
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
    # 将集合切分成两个子集并返回
    return mat0,mat1

def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
    '''数据集,建立叶节点的函数,误差计算函数,包含构建树所需的其他参数的元组'''
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)#choose the best split
    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. 复杂数据的局部建模

    

      决策树不断将数据切分成小数据集,知道目标变量完全相同,或则数据不能再切分为止。决策树是一种贪心算法,它要在给定的时间内作出最佳选择而不管能否达到全局最优。

      CART是一个使用很广泛的树构建算法,它使用二元切分来处理连续变量。对CART稍作修改就可以处理回归问题。下面将实现CART算法和回归树。回归树与分类树思路相似,但叶节点的数据类型不是散型而是连续型。

        

        

     2.连续和离散型特征的树的构建

        在树的构建中,需要解决多种类型数据的存储问题。将使用字典来存储树的数据结构,该字典包含以下四个元素:

        1. 待切分的特征

        2. 待切分的特征值

        3. 右子树

        4. 左子树

        CART算法只做二元切分,所以这里可以固定树的数据结构。树包含左键和右键,可以存储另一棵子树或则单个值。字典还包括特征和特征值。可以用下面的代码来建立树节点:

class treeNode():
    def __init__(self, feat, val, right, left):
        featureToSplitOn = feat
        valueOfSplit = val
        rightBranch = right
        leftBranch = left

         本章可以构建两种树:第一种是回归树,其叶节点包含单个值;第二种是模型树,其每个节点包含一个线性方程。创建这两种树时,我们尽量使得代码之间可以重用。

        

        CART树实现代码: 

def loadDataSet(fileName):
    fr = open(fileName)
    dataMat = []
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = map(float, curLine)
        dataMat.append(fltLine)
    return dataMat

def binSplitDataSet(dataSet, feature, value):
    '''数据集,待切分的特征,该特征的某个值'''
    # np.nonzero函数用于得到数组array中非零元素的位置(数组索引)
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
    # 将集合切分成两个子集并返回
    return mat0,mat1

def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
    '''数据集,建立叶节点的函数,误差计算函数,包含构建树所需的其他参数的元组'''
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)#choose the best split
    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 

    函数createTree()是一个递归函数,该函数首先将数据集分成两个部分,切分由函数chooseBestSplit()完成,这里还未给出该函数的实现。如归满足停止条件,该函数将会返回None和某类模型的值。如果构建的是回归树,该模型是一个常数。如果是模型树,其模型是一个线性方程。

    我们运行一下按指定列的某个值来切分矩阵的程序的结果:

    

    下面给出回归树的chooseBestSplit()函数。针对回归树的构建来给chooseBestSplit()函数加入具体的代码。

    

   3.将CART算法用于回归

    要对数据的复杂关系进行建模,我们决定使用树结构来帮助切分数据,那么如何实现数据的切分呢?怎么才能知道数据是否充分的切分了呢?这些都取决于叶节点的建模方式。回归树假设叶节点是常数值,这种策略认为数据中的复杂关系可以用树结构来概括。

    为成功构建以分段常数为叶节点的树,需要度量出数据的一致性。之前我们使用树进行分类时,会给定节点时计算数据的混乱度。那么如何计算连续型数值的混乱度呢?首先计算所有数据的均值,然后计算每条数据的值到均值的差值。为了对正负差值同等看待,一般使用绝对值或平方值代替上述差值。这里需要的是平方误差的总值(总方差)。总方差可以通过均方差乘以数据集中样本点个数来得到。    

   3.1 构建树

    首先实现chooseBestSplit()函数。给定某个误差计算方法,该函数会找到数据集上的最佳二元切分方式。另外,该函数还要确定什么时候停止切分,一旦停止切分会生成一个叶节点。因此chooseBestSplit()函数需要完成两件事:用最佳方式切分数据集和生成相应的叶节点。
    除数据集外,函数chooseBestSplit()还有leafType,errType和ops这三个参数。leafType是对创建叶节点的函数的引用,errType是对前面介绍的总方差计算函数的引用,ops是用户定义的参数构成的元组,用以完成对树的构建。
    伪代码如下:
        

    下面给出上述三个函数的具体实现代码:

def regLeaf(dataSet):
    '''生成叶节点 得到叶节点的模型。在回归树中该模型就是目标变量的均值'''
    return mean(dataSet[:,-1])

def regErr(dataSet):
    '''在给定数据上计算目标变量的平方误差
        var()均方差函数 乘以 数据集中的样本个数
    '''
    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()[0])) == 1:#倒数第一列转化成list 不重复
        return None,leafType(dataSet)  #如果剩余特征数为1,停止切分1。
        # 找不到好的切分特征,调用regLeaf直接生成叶结点
    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]): python3报错修改为下面
        for splitVal in set((dataSet[:, featIndex].T.A.tolist())[0]):#遍历每个特征里不同的特征值
            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) #停止切分2
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    #判断切分后子集大小,小于最小允许样本数停止切分3
    if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
        return None, leafType(dataSet)
    return bestIndex,bestValue#返回特征编号和用于切分的特征值
    

    3.2 运行代码

    
       
    
        9-2的数据存构建的回归树如下:

        

   4. 树剪枝

      一棵树如果节点过多,表明该模型可能对数据进行了‘过拟合’。前面章节使用了在测试集上某种交叉验证技术来发现过拟合,决策树亦是如此。

      通过降低决策树的复杂度来避免过拟合的过程称为剪枝。其实之前我们已经进行过剪枝了。在函数 chooseBestSplit中的提前终止条件,实际上一种所谓的预剪枝操作。另一种剪枝操作需要训练集和测试集,称作后剪枝。

    

   4.1  预剪枝

    上节的两个实验的结果背后存在一些问题。树构建算法对输入的参数tols和toln十分敏感,使用其他值不太容易达到这么好的效果。

    ...

    与之前只有两个节点的树相比,这里的太过臃肿,它甚至为了数据集中每个样本都分配了一个节点。

    ...

    

        图9-3中的散点图与9-1十分相似,但是数量级差了100倍。而9-1的图分出来树只有两个节点,而这里构建出的节点有很多节点。产生这个现象的原因在于,停止条件tols对误差的数量级十分敏感。如果在选项中花费时间并对上述误差的容忍取平方值,或许也能得到仅有两个叶节点组成的树:

        

        然而,通过不断修改停止条件来得到合理结果并不是一个很好的办法。下面将讨论后剪枝,是一个更理想的剪枝方法。

    

    4.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 we have no test data collapse the tree
    if (isTree(tree['right']) or isTree(tree['left'])):#if the branches are not trees try to prune them
        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 they are now both leafs, see if we can merge them
    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

    这里假设发生了过拟合,从而对树进行剪枝。

    接下来检查某个分支到底是子树还是节点。如果是子树,就调用pure来对该子树进行剪枝。对左右两个分支剪完之后,还需要检查他们是否仍还是子树。如果两个分支已经不再是子树,那么就可以合并。具体做法是对合并前后的误差进行比较。如果合并之后的误差比不合并的误差小就进行合并操作。

    

    可以看到,大量的节点已经被剪掉了,但是没有预期的那样剪枝成两个部分,说明后剪枝可能不如预剪枝有效。一般来说,为了寻求最佳模型可以同时使用两个剪枝技术。

    



  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值