机器学习第九章树回归

引言

上一章的线性回归包含了一些强大的方法,但这些方法创建的模型需要拟合所有的样本点(局部加权线性回归除外)。
当数据拥有众多特征并且特征之间关系十分复杂时,构建全局模型的想法就显得太难了,也略显笨拙。而且,实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。
CART(Classification And Regression Trees, 分类回归树)该算法既可以用于分类还可以用于回归。

树回归的优缺点
优点:可以对复杂和非线性的数据建模。
缺点:结果不易理解。
适用数据类型:数值型和标称型数据。

树回归的一般方法
1 收集数据: 采用任意方法收集数据。
2 准备数据:需要数值型的数据,标称型数据应该映射成二值型数据。
3 分析数据: 绘出数据的二维可视化显示结果,以字典方式生成树。
4 训练算法 :大部分时间都花费在叶节点树模型的构建上。
5 测试算法 :使用测试数据上的^ 值来分析模型的效果。
6 使用算法:使用训练出的树做预测,预测結果还可以用来做很多事情

9.1复杂数据的局部性建模

CART是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量。对CART稍作修改就可以处理回归问题。曾经我们使用香农熵来度量集合的无组织程度。如果选用其他方法来代替香农熵,就可以使用树构建算法来完成回归。
下面将实现CART算法和回归树。回归树与分类树的思路类似,但叶节点的数据类型不是离散型,而是连续型。

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

在树的构建过程中,需要解决对哦中类型数据的存储问题。我们将使用一部字典来存储树的数据结构,该字典将包含以下4个元素:

1 待切分的特征。
2 待切分的特征值。
3 右子树。当不再需要切分的时候,也可以是单个值。
4 左子树。与右子树类似。

这与第3章的树结构有一点不同。第3章用一部字典来存储每个切分,但该字典可以包含两个或两个以上的值。而CART算法只做二元切分,所以这里可以固定树的数据结构。树包含左键和右键,可以存储另一棵子树或者单个值。字典还包含特征和特征值这两个键,它们给出切分算法所有的特征和特征值。当然,读者可以用面向对象的编程模式来建立这个数据结构。例如,可以用下面的Python代码来建立树节点:

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

面先给出两种树构建算法中的一些共用代码。函数 createTree() 的伪代码大致如下:
在这里插入图片描述
新建文件regTrees.py并添加如下代码:

from numpy import *
 
def loadDataSet(fileName):      #general function to parse tab -delimited floats
    dataMat = []                #assume last column is target value
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = map(float,curLine) #map all elements to float()
        dataMat.append(fltLine)
    return dataMat
 
def binSplitDataSet(dataSet, feature, value):
    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)):#assume dataSet is NumPy Mat so we can array filtering
    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  

loadDataSet()函数与其他章节中同名函数功能类似。在前面的章节中,目标变量会单独存放其自己的列表中,但这里的数据会存放在一起。该函数读取一个以tab键位分隔符的文件,然后将每行的内容保存成一组浮点数。
函数 binSplitDataSet() 有3个参数:数据集合、待切分的特征和该特征的某个值。在给定特征和特征值的情况下,该函数通过数组过滤方式将上述数据集合切分得到两个子集并返回。
最后一个函数是树构建函数 creatTree(),它有4个参数:数据集合其他3个可选参数。这些可选参数决定了树的类型:leafType给出建立叶节点的函数;errType代表误差计算函数;而ops是一个包含树构建所需其他参数的元组。
函数 creatTree() 是一个递归函数。该函数首先尝试将数据集分成两个部分,切分由函数chooseBestSplit() 完成。如果满足停止条件,chooseBestSplit() 将返回None和某类模型的值。如果构建的是回归树,该模型是一个常数。如果是模型树,其模型是一个线性方程。后面会看到停止条件的作用方式。如果不满足停止条件,chooseBestSplit() 将创建一个新的Python字典并将数据集分成两份,在这两份数据集上将分别继续递归调用 createTree() 函数。

import regTrees
from numpy import *
# 创建4阶对角矩阵
testMat = mat(eye(4))
mat0,mat1 = regTrees.binSplitDataSet(testMat, 1, 0.5)
print(mat0)
print(mat1)

实验结果如下所示
在这里插入图片描述

9.3将CART算法用于回归

9.3.1构建树

回归树假设叶节点是常数值,这种策略认为数据中的复杂关系可以用树结构来概括。
为成功构建以分段常数为叶节点的树,需要度量出数据的一致性。
在数据集上计算混乱度,首先计算所有数据的均值,然后计算每条数据的值到均值的差值(一般使用绝对值或平方值)。
找到数据集切分的最佳位置函数chooseBestSplit(),该函数遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。
chooseBestSplit() 函数的目标是找到数据集切分的最佳位置。它遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。该函数的伪代码大致如下:
在这里插入图片描述
在regTrees.py文件中加入下列代码:

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 all the target variables are the same value: quit and return value
    if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #exit cond 1
        return None, leafType(dataSet)
    m,n = shape(dataSet)
    #the choice of the best feature is driven by Reduction in RSS error from mean
    S = errType(dataSet)
    bestS = inf; bestIndex = 0; bestValue = 0
    for featIndex in range(n-1):
        for splitVal in set((dataSet[:,featIndex].T.A.tolist())[0]):# tolist将数组和矩阵转化为列表
            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 the decrease (S-bestS) is less than a threshold don't do the split
    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):  #exit cond 3
        return None, leafType(dataSet)
    return bestIndex,bestValue#returns the best feature to split on
                              #and the value used for that split

第一个函数 regLeaf() ,它负责生成叶节点。当 chooseBestSplit() 函数确定不再对数据进行切分时,将调用该 regLeaf() 函数来得到叶节点的模型。在回归树中,该模型其实就是目标变量的均值。
第二个函数是误差估计函数 regErr()。该函数在给定数据上计算目标变量的平方误差。当然也可以先计算出均值,然后计算每个差值再平方。但这里直接调用均方差函数 var() 更加方便。因为这里需要返回的是总方差,所以要用均方差乘以数据集中样本的个数。
第三个函数是 chooseBestSplit() ,它是回归树构建的核心函数。该函数的目的是找到数据的最佳二元切分方式。如果找不到一个“好”的二元切分,该函数返回None并同时调用 createTree() 方法来产生叶节点,叶节点的值也将返回None。

import regTrees
from numpy import *
myDat = regTrees.loadDataSet('ex00.txt.')
myMat = mat(myDat)
print(regTrees.createTree(myMat))

在这里插入图片描述

import regTrees
from numpy import *
myDat1 = regTrees.loadDataSet('ex0.txt')
myMat1 = mat(myDat1)
print(regTrees.createTree(myMat1))

在这里插入图片描述

9.4树剪枝

通过降低决策树的复杂度来避免过拟合的过程称为剪枝(pruning)。在函数chooseBestSplit()中的提前终止条件,实际上是在进行一种所谓的预剪枝 (prepruning)操作。另一种形式的剪枝需要使用测试集和训练集,称作后剪枝(postpruning ) 。

9.4.1预剪枝

预剪枝就是及早的停止树增长,在构造决策树的同时进行剪枝。所有决策树的构建方法,都是在无法进一步降低熵的情况下才会停止创建分支的过程,为了避免过拟合,可以设定一个阈值,熵减小的数量小于这个阈值,即使还可以继续降低熵,也停止继续创建分支。但是这种方法实际中的效果并不好。

9.4.2后剪枝

使用后剪枝方法需要将数据集分成测试集和训练集。首先指定参数,使得构建出的树足够大、足够复杂,便于剪枝。接下来从上而下找到叶节点,用测试集来判断这些叶节点合并是否能降低测试误差。如果是的话就合并。
函数prune()的伪代码如下:
基于已有的树切分测试数据:
    如果存在任一子集是一棵树,则在该子集递归剪枝过程
    计算将当前两个叶节点合并后的误差
    计算不合并的误差
    如果合并会降低误差的话,就将叶节点合并
 在regTrees.py中添加下列代码:

# 判断节点是否是一个字典
def isTree(obj):
    """
    Desc:
        测试输入变量是否是一棵树,即是否是一个字典
    Args:
        obj -- 输入变量
    Returns:
        返回布尔类型的结果。如果 obj 是一个字典,返回true,否则返回 false
    """
    return (type(obj).__name__ == 'dict')


# 计算左右枝丫的均值
def getMean(tree):
    """
    Desc:
        从上往下遍历树直到叶节点为止,如果找到两个叶节点则计算它们的平均值。
        对 tree 进行塌陷处理,即返回树平均值。
    Args:
        tree -- 输入的树
    Returns:
        返回 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):
    """
    Desc:
        从上而下找到叶节点,用测试数据集来判断将这些叶节点合并是否能降低测试误差
    Args:
        tree -- 待剪枝的树
        testData -- 剪枝所需要的测试数据 testData
    Returns:
        tree -- 剪枝完成的树
    """
    # 判断是否测试数据集没有数据,如果没有,就直接返回tree本身的均值
    if np.shape(testData)[0] == 0:
        return getMean(tree)

    # 判断分枝是否是dict字典,如果是就将测试数据集进行切分
    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)

    # 上面的一系列操作本质上就是将测试数据集按照训练完成的树拆分好,对应的值放到对应的节点

    # 如果左右两边同时都不是dict字典,也就是左右两边都是叶节点,而不是子树了,那么分割测试数据集。
    # 1. 如果正确
    #   * 那么计算一下总方差 和 该结果集的本身不分枝的总方差比较
    #   * 如果 合并的总方差 < 不合并的总方差,那么就进行合并
    # 注意返回的结果: 如果可以合并,原来的dict就变为了 数值
    if not isTree(tree['left']) and not isTree(tree['right']):
        lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
        # power(x, y)表示x的y次方
        errorNoMerge = sum(np.power(lSet[:, -1] - tree['left'], 2)) + sum(np.power(rSet[:, -1] - tree['right'], 2))
        treeMean = (tree['left'] + tree['right'])/2.0
        errorMerge = sum(np.power(testData[:, -1] - treeMean, 2))
        # 如果 合并的总方差 < 不合并的总方差,那么就进行合并
        if errorMerge < errorNoMerge:
            print("merging")
            return treeMean
        else:
            return tree
    else:
        return tree

import regTrees
from numpy import *



myDat = regTrees.loadDataSet('ex2.txt')
myMat2 = mat(myDat)
mytree=regTrees.createTree(myMat2,ops=(0,1))
myDatTest = regTrees.loadDataSet('ex2test.txt')
myMat2Test = mat(myDatTest)
print(regTrees.prune(mytree,myMat2Test))



程序中包含三个函数:isTree()、geyMean()和 prune()。其中i sTree() 用于测试输入变量是否是一棵树,返回布尔类型的结果。即用于判断当前处理的节点是否是叶节点。
函数 getMean() 是一个递归函数,它从上往下遍历树直到叶节点为止。如果找到两个叶节点则计算它们的平均值。该函数对树进行塌陷处理(即返回树平均值),在 prune() 函数中调用该函数时应明确这一点。
prune() 是主函数,它有两个参数:待剪枝的树与剪枝所需的测试数据testData。prune() 函数首先需要确认测试集是否为空。一旦非空,则反复递归调用函数 prune() 对测试数据进行切分。因为树是由其他数据集(训练集)生成的,所以测试集上会有一些样本与原数据集样本的取值范围不同。一旦出现这种情况应当怎么办?数据发生过拟合应该进行剪枝吗?或者模型正确不需要任何剪枝?这里假设发生了过拟合,从而对树进行剪枝。
接下来要检查某个分支到底是子树还是节点。如果是子树,就调用函数 prune() 来对该子树进行剪枝。在对左右两个分支完成剪枝之后,还需要检查它们是否仍然还是子树。如果两个分支已经不再是子树,那么就可以进行合并。具体做法是对合并前后的误差进行比较。如果合并后的误差比不合并的误差小就进行合并操作,反之则不合并直接返回

在这里插入图片描述

实验分析:
本次实验出来的结果还是和书上有些不一样,并且代码的部分也是没有看的很明白,这正是自己所要提升的部分。

9.5模型树

因为树回归每个节点是一些特征和特征值,选取的原则是根据特征方差最小。用树来对数据建模,除了把叶节点设定为常数值之外,还可以把叶节点设置为分段线性函数,这就是模型树。如下图可用2条直线模型来拟合。

在这里插入图片描述
下一个问题就是,为了找到最佳切分,应该怎样计算误差呢?前面用于回归树的误差计算方法这里不能再用。稍加变化,对于给定的数据集,应该先用线性的模型来对它进行拟合,然后计算真实的目标值与预模型预测值间的差值。最后将这些差值的平方求和就得到了所需的误差。

def linearSolve(dataSet):   #helper function used in two places
    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,\n\
        try increasing the second value of ops')
    ws = xTx.I * (X.T * Y)
    return ws,X,Y
 
def modelLeaf(dataSet):#create linear model and return coeficients
    ws,X,Y = linearSolve(dataSet)
    return ws
 
def modelErr(dataSet):
    ws,X,Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(power(Y - yHat,2))

函数 linearSolve() 会被其他两个函数调用。其主要功能是将数据集格式化成目标变量Y和自变量X。X和Y用于执行简单的线性回归。另外在这个函数中也应当注意,如果矩阵的逆不存在也会造成程序异常。
第二个函数 modelLeaf() 与函数 regLeaf() 类似,当数据不再需要切分的时候它负责生成叶节点的模型。该函数在数据集上调用 linearSolve() 并返回回归系数ws。
最后一个函数是 modelErr() ,可以在给定的数据集上计算误差。它与函数 regErr() 类似,会被chooseBestSplit() 调用来找到最佳的切分。该函数在数据集上调用 linearSolve() ,之后返回yHat和Y之间的平方误差。

import regTrees
from numpy import *
myMat2 = mat(regTrees.loadDataSet('exp2.txt'))
print(regTrees.createTree(myMat2,regTrees.modelLeaf,regTrees.modelErr,(1,10)))

实验结果如下所示
在这里插入图片描述

9.6小结

本章学习了CART算法并构建了二元树。学习了预剪枝(在树的构建过程中就进行剪枝)和后剪枝(当树构建完毕再进行剪枝)的相关概念与代码实现,最后比较了树回归与之前的线性回归。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值