机器学习 树回归

树回归

分类回归树(Classification And Regression Trees,CART)是一种构造树的监督学习方法。

和ID3决策树作比较:

1. ID3每次直接用最佳特征分割数据,即如果当前特征有4个可能值,那么数据将被分成4份,处理的是标称型数据,不能直接处理连续型数据。CART则利用二元切分来处理连续型变量,每次会找一个最佳特征的阈值,把数据集分成两部分,也就是左子树和右子树。

2. CART使用方差计算来代替香农熵。但目的都是找最佳切分特征
树回归将数据集切分成多份易建模的数据,然后利用线性回归进行建模和拟合。

寻找最佳特征算法伪代码:

	如果该数据集的特征值只有一种,不可切分,返回当前结点的数据均值作为特征值

	否则重复一下步骤直到找到最小总方差

		遍历每一列

		遍历每列的值

			用该值切分数据

			计算总方差

			如果总方差差值小于最初设定的阈值,不可切分

			如果左右样本数小于最初设定的阈值,不可切分

	否则返回最佳特征和最佳特征值。
构建回归树算法伪代码:

寻找当前最佳待切特征和特征值并返回

	如果当前最佳特征没有找到,不可切分,则把当前结点的数据均值作为叶节点

	否则用最佳特征和特征值构建当前结点

	切分后的左右节点分别递归以上算法

需要输入的参数有:数据集,叶节点模型函数(均值),误差估计函数(总方差),允许的总方差最小下降值,节点最小样本数。

1.2、树构建算法 比较

我们在 第3章 中使用的树构建算法是 ID3 。ID3 的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有 4 种取值,那么数据将被切分成 4 份。一旦按照某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。另外一种方法是二元切分法,即每次把数据集切分成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入树的右子树。

除了切分过于迅速外, ID3 算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征转换成离散型,才能在 ID3 算法中使用。但这种转换过程会破坏连续型变量的内在性质。而使用二元切分法则易于对树构造过程进行调整以处理连续型特征。具体的处理方法是: 如果特征值大于给定值就走左子树,否则就走右子树。另外,二元切分法也节省了树的构建时间,但这点意义也不是特别大,因为这些树构建一般是离线完成,时间并非需要重点关注的因素。

CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量。对 CART 稍作修改就可以处理回归问题。第 3 章中使用香农熵来度量集合的无组织程度。如果选用其他方法来代替香农熵,就可以使用树构建算法来完成回归。

回归树与分类树的思路类似,但是叶节点的数据类型不是离散型,而是连续型。

实现代码
#!/usr/bin/env python
# encoding: utf-8
from numpy import *
def loadDataSet(filename):
    '''
        curLine : 当前分割后的字符串数组
        fltLine : 被映射成浮点数型的数组
    :param filename: 文件路径
    :return:
        dataMat : 数据集
    '''
    dataMat = []
    fr = open(filename)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = map(float, curLine)
        print("curLine :" + curLine, end= "\n")
        print("fltLine :" + fltLine, end= "\b")
        dataMat.append(fltLine)
    return dataMat

def binSplitDataSet(dataSet, feature, value):
    '''
    :param dataSet:
    :param feature: 特征向量的列数
    :param value: 比较的值
    :return:
        mat0: 矩阵中特征向量列大于value值的元素所组成的矩阵
        mat1: 矩阵中特征向量列大于value值的元素所组成的矩阵
    '''
    # nonzero函数返回array中非零元素的索引 二维表示为(行索引, 列索引)
    mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0], :]
    # print('dataSet[:, feature]', dataSet[:, feature])
    # print('nonzero(dataSet[:, feature] > value)', nonzero(dataSet[:, feature] > value))
    # print('dataSet[nonzero(dataSet[:, feature] > value)[0], :]', dataSet[nonzero(dataSet[:, feature] > value)[0], :])
    mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :]
    # print('dataSet[:, feature]', dataSet[:, feature])
    # print('nonzero(dataSet[:, feature] > value)', nonzero(dataSet[:, feature] > value))
    # print('dataSet[nonzero(dataSet[:, feature] > value)[0], :]', dataSet[nonzero(dataSet[:, feature] > value)[0], :])
    return mat0, mat1

def regLeaf(dataSet):
    '''
        用于生成叶节点, 理解是通过求均值,来用中心点代表这类数据
    :param dataSet:
    :return:
    '''
    #计算指定轴上的算术平均值 默认为将向量组展开成一行所得的算数平均值
    #`mean(a, axis=None, dtype=None, out=None, keepdims=np._NoValue)`
    #a包含期望平均值的数组 不是数组需转化 axis 指定轴 dtype平均值类型 keepdims如果将其设置为True,则缩小的轴将保留为尺寸1的尺寸。使用此选项,结果将针对输入数组正确广播。
    return mean(dataSet[:, 1])

def regErr(dataSet):
    '''
        用于估计误差 计算目标变量的平方误差 通过决策树划分, 可以让靠近的数据分到同一类中
    :param dataSet:
    :return:
        平方差之和, 即总方差
    '''
    #计算指定轴上的方差 默认为将向量组展开成一行所得的方差
    return var(dataSet[:, 1]) * shape(dataSet)[0]


def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops = (1,4)):
    '''
        找出数据最佳的二分切分方式, 如果找不到一个好的二元分割,会返回None,并产生叶节点
        tolS 为允许的误差下降值
        tolN 为最小的切割样本数
    :param dataSet:
    :param leafType:
    :param errType:
    :param ops:
    :return:
            bestIndex feature的index坐标
            bestValue 切分的最优值
    '''
    tolS = ops[0]
    tolN = ops[1]
    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
    #featIndex 某一列
    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)
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    if(shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
        return None, leafType(dataSet)
    return bestIndex, bestValue

def createTree(dataSet, leafType = regLeaf, errType = regErr, ops = (1, 4)):
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
    if feat == None: 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


def isTree(obj):
    '''
        测试变量是否是一棵树
    :param obj: 输入变量
    :return:
        布尔类型结果。如果obj是一个字典返回true
    '''
    return (type(obj).__name__ == 'dict')

def getMean(tree):
    '''
        遍历树的结点 直到叶节点为止,如果找到两个叶节点就计算它们的平均值
    :param tree:
    :return:
    '''
    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['rightt'], 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

def modelLeaf(dataSet):
    """
    Desc:
        当数据不再需要切分的时候,生成叶节点的模型。
    Args:
        dataSet -- 输入数据集
    Returns:
        调用 linearSolve 函数,返回得到的 回归系数ws
    """
    ws, X, Y = linearSolve(dataSet)
    return ws


# 计算线性模型的误差值
def modelErr(dataSet):
    """
    Desc:
        在给定数据集上计算误差。
    Args:
        dataSet -- 输入数据集
    Returns:
        调用 linearSolve 函数,返回 yHat 和 Y 之间的平方误差。
    """
    ws, X, Y = linearSolve(dataSet)
    yHat = X * ws
    # print corrcoef(yHat, Y, rowvar=0)
    return sum(power(Y - yHat, 2))


 # helper function used in two places
def linearSolve(dataSet):
    """
    Desc:
        将数据集格式化成目标变量Y和自变量X,执行简单的线性回归,得到ws
    Args:
        dataSet -- 输入数据
    Returns:
        ws -- 执行线性回归的回归系数
        X -- 格式化自变量X
        Y -- 格式化目标变量Y
    """
    m, n = shape(dataSet)
    # 产生一个关于1的矩阵
    X = mat(ones((m, n)))
    Y = mat(ones((m, 1)))
    # X的0列为1,常数项,用于计算平衡误差
    X[:, 1: n] = dataSet[:, 0: n-1]
    Y = dataSet[:, -1]

    # 转置矩阵*矩阵
    xTx = X.T * X
    # 如果矩阵的逆不存在,会造成程序异常
    if linalg.det(xTx) == 0.0:
        raise NameError('This matrix is singular, cannot do inverse,\ntry increasing the second value of ops')
    # 最小二乘法求最优解:  w0*1+w1*x1=y
    ws = xTx.I * (X.T * Y)
    return ws, X, Y


# 回归树测试案例
# 为了和 modelTreeEval() 保持一致,保留两个输入参数
def regTreeEval(model, inDat):
    """
    Desc:
        对 回归树 进行预测
    Args:
        model -- 指定模型,可选值为 回归树模型 或者 模型树模型,这里为回归树
        inDat -- 输入的测试数据
    Returns:
        float(model) -- 将输入的模型数据转换为 浮点数 返回
    """
    return float(model)


# 模型树测试案例
# 对输入数据进行格式化处理,在原数据矩阵上增加第0列,元素的值都是1,
# 也就是增加偏移值,和我们之前的简单线性回归是一个套路,增加一个偏移量
def modelTreeEval(model, inDat):
    """
    Desc:
        对 模型树 进行预测
    Args:
        model -- 输入模型,可选值为 回归树模型 或者 模型树模型,这里为模型树模型,实则为 回归系数
        inDat -- 输入的测试数据
    Returns:
        float(X * model) -- 将测试数据乘以 回归系数 得到一个预测值 ,转化为 浮点数 返回
    """
    n = shape(inDat)[1]
    X = mat(ones((1, n+1)))
    X[:, 1: n+1] = inDat
    # print X, model
    return float(X * model)


# 计算预测的结果
# 在给定树结构的情况下,对于单个数据点,该函数会给出一个预测值。
# modelEval是对叶节点进行预测的函数引用,指定树的类型,以便在叶节点上调用合适的模型。
# 此函数自顶向下遍历整棵树,直到命中叶节点为止,一旦到达叶节点,它就会在输入数据上
# 调用modelEval()函数,该函数的默认值为regTreeEval()
def treeForeCast(tree, inData, modelEval=regTreeEval):
    """
    Desc:
        对特定模型的树进行预测,可以是 回归树 也可以是 模型树
    Args:
        tree -- 已经训练好的树的模型
        inData -- 输入的测试数据,只有一行
        modelEval -- 预测的树的模型类型,可选值为 regTreeEval(回归树) 或 modelTreeEval(模型树),默认为回归树
    Returns:
        返回预测值
    """
    if not isTree(tree):
        return modelEval(tree, inData)
    # 书中写的是inData[tree['spInd']],只适合inData只有一列的情况,否则会产生异常
    if inData[0, tree['spInd']] <= tree['spVal']:
        # 可以把if-else去掉,只留if里面的分支
        if isTree(tree['left']):
            return treeForeCast(tree['left'], inData, modelEval)
        else:
            return modelEval(tree['left'], inData)
    else:
        # 同上,可以把if-else去掉,只留if里面的分支
        if isTree(tree['right']):
            return treeForeCast(tree['right'], inData, modelEval)
        else:
            return modelEval(tree['right'], inData)


# 预测结果
def createForeCast(tree, testData, modelEval=regTreeEval):
    """
    Desc:
        调用 treeForeCast ,对特定模型的树进行预测,可以是 回归树 也可以是 模型树
    Args:
        tree -- 已经训练好的树的模型
        testData -- 输入的测试数据
        modelEval -- 预测的树的模型类型,可选值为 regTreeEval(回归树) 或 modelTreeEval(模型树),默认为回归树
    Returns:
        返回预测值矩阵
    """
    m = len(testData)
    yHat = mat(zeros((m, 1)))
    # print yHat
    for i in range(m):
        yHat[i, 0] = treeForeCast(tree, mat(testData[i]), modelEval)
        # print "yHat==>", yHat[i, 0]
    return yHat


if __name__ == "__main__":
    # 测试数据集
    testMat = mat(eye(4))
    print(testMat)
    print(type(testMat))
    mat0, mat1 = binSplitDataSet(testMat, 1, 0.5)
    print(mat0, '\n-----------\n', mat1)

    # # 回归树
    # myDat = loadDataSet('data/9.RegTrees/data1.txt')
    # # myDat = loadDataSet('data/9.RegTrees/data2.txt')
    # # print 'myDat=', myDat
    # myMat = mat(myDat)
    # # print 'myMat=',  myMat
    # myTree = createTree(myMat)
    # print myTree

    # # 1. 预剪枝就是:提起设置最大误差数和最少元素数
    # myDat = loadDataSet('data/9.RegTrees/data3.txt')
    # myMat = mat(myDat)
    # myTree = createTree(myMat, ops=(0, 1))
    # print myTree

    # # 2. 后剪枝就是:通过测试数据,对预测模型进行合并判断
    # myDatTest = loadDataSet('data/9.RegTrees/data3test.txt')
    # myMat2Test = mat(myDatTest)
    # myFinalTree = prune(myTree, myMat2Test)
    # print '\n\n\n-------------------'
    # print myFinalTree

    # # --------
    # # 模型树求解
    # myDat = loadDataSet('data/9.RegTrees/data4.txt')
    # myMat = mat(myDat)
    # myTree = createTree(myMat, modelLeaf, modelErr)
    # print myTree

    # # # 回归树 VS 模型树 VS 线性回归
    # trainMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_train.txt'))
    # testMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_test.txt'))
    # # # 回归树
    # myTree1 = createTree(trainMat, ops=(1, 20))
    # print myTree1
    # yHat1 = createForeCast(myTree1, testMat[:, 0])
    # print "--------------\n"
    # # print yHat1
    # # print "ssss==>", testMat[:, 1]
    # # corrcoef 返回皮尔森乘积矩相关系数
    # print "regTree:", corrcoef(yHat1, testMat[:, 1],rowvar=0)[0, 1]

    # # 模型树
    # myTree2 = createTree(trainMat, modelLeaf, modelErr, ops=(1, 20))
    # yHat2 = createForeCast(myTree2, testMat[:, 0], modelTreeEval)
    # print myTree2
    # print "modelTree:", corrcoef(yHat2, testMat[:, 1],rowvar=0)[0, 1]

    # # 线性回归
    # ws, X, Y = linearSolve(trainMat)
    # print ws
    # m = len(testMat[:, 0])
    # yHat3 = mat(zeros((m, 1)))
    # for i in range(shape(testMat)[0]):
    #     yHat3[i] = testMat[i, 0]*ws[1, 0] + ws[0, 0]
    # print "lr:", corrcoef(yHat3, testMat[:, 1],rowvar=0)[0, 1]


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值