树回归(源码实现)

#coding=utf-8


'''
Created on Feb 4, 2011
Tree-Based Regression Methods
@author: Peter Harrington


树回归

分类回归树CART
策树是一种贪心算法,它要在给定时间内做出最佳选择,但
并不关心能否达到全局最优。
优点:可以时复杂和非线性的数据建模。
缺点:结果不易理解。
适用数据类型:数值型和标称型数据。

    第3章使用的树构建算法是ID3 o ID3的做法是每次选取当前最佳的特征来分割数据,并按照
该特征的所有可能取值来切分。也就是说,如果一个特征有4种取值,那么数据将被切成4份。一‘
旦按某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分
方式过于迅速。另外一种方法是二元切分法,即每次把数据集切成两份。如果数据的某特征值等
于切分所要求的值,那么这些数据就进人树的左子树,反之则进人树的右子树。
    除了切分过于迅速外,ID3算法还存在另一个问题,它不能直接处理连续型特征。只有事先
将连续型特征转换成离散型,才能在ID3算法中使用。但这种转换过程会破坏连续型变量的内在
性质。而使用二元切分法则易于对树构建过程进行调整以处理连续型特征。具体的处理方法是:
如果特征值大于给定值就走左子树,否则就走右子树。另外,二元切分法也节省了树的构建时间,
但这点意义也不是特别大,因为这些树构建一般是离线完成,时间并非需要重点关注的因素。

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

连续和离散型特征的树的构建
在树的构建过程中,需要解决多种类型数据的存储问题。与第3章类似,这里将使用一部字
典来存储树的数据结构,该字典将包含以下4个元素。
1、待切分的特征。
2、待切分的特征值。
3、右子树。当不再需要切分的时候,也可以是单个值。
4、左子树。与右子树类似。

    这与第3章的树结构有一点不同。第3章用一部字典来存储每个切分,但该字典可以包含两个
或两个以上的值。而CART算法只做二元切分,所以这里可以固定树的数据结构。树包含左键和
右键,可以存储另一棵子树或者单个值。字典还包含特征和特征值这两个键,它们给出切分算法
所有的特征和特征值。



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



    本章将构建两种树:第一种是9.4节的回归树(regression tree ),其每个叶节点包含单个值;
第二种是9.5节的模型树(model tree ),其每个叶节点包含一个线性方程。

函数createTree()的伪代码大致如下:
找到最佳的待切分特征:
    如果该节点不能再分,将该节点存为叶节点
    执行二元切分
    在右子树调用createTree()方法
    在左子树调用createTree()方法


'''


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

'''
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],:][0]
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
    return mat0,mat1

'''
*********************************
createTree 还有问题
*********************************
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
'''
'''
    上述代码中,函数chooseBestSplit()最复杂,该函数的目标是找到数据集切分的最佳
位置。它遍历所有的特征及其可能的取值来找到使误差最小化的切分阑值。该函数的伪代码大
致如下:
    对每个特征:
        对每个特征值:
            将数据集切分成两份
            计算切分的误差
            如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
    返回最佳切分的特征和闺值

回归树的切分函数

    上述程序清单中的第一个函数是regLeaf ( ),它负责生成叶节点。当chooseBestSplit()
函数确定不再对数据进行切分时,将调用该regLeaf()函数来得到叶节点的模型。在回归树中,
该模型其实就是目标变量的均值。
    第二个函数是误差估计函数regErr()。该函数在给定数据上计算目标变量的平方误差。当
然也可以先计算出均值,然后计算每个差值再平方。但这里直接调用均方差函数var()更加方便。

因为这里需要返回的是总方差,所以要用均方差乘以数据集中样本的个数。
    第三个函数是chooseBestSplit(),它是回归树构建的核心函数。该函数的目的是找到数
据的最佳二元切分方式。如果找不到一个“好”的二元切分,该函数返回None并同时调用
crea七eTree()方法来产生叶节点,叶节点的值也将返回None。接下来将会看到,在函数
chooseBestSplit()中有三种情况不会切分,而是直接创建叶节点。如果找到了一个“好”的切
分方式,则返回特征编号和切分特征值。
    函数chooseBestSplit()一开始为。p。设定了to1S和七。1N这两个值。它们是用户指定的参
数,用于控制函数的停止时机。其中变量to1S是容许的误差下降值,to1N是切分的最少样本数。
接下来通过对当前所有目标变量建立一个集合,函数chooseBestSplit()会统计不同剩余特征
值的数目。如果该数目为1,那么就不需要再切分而直接返回.。然后函数计算了当前数据集的
大小和误差。该误差S将用于与新切分误差进行对比,来检查新切分能否降低误差。

    这样,用于找到最佳切分的几个变量就被建立和初始化了。下面就将在所有可能的特征及其
可能取值上遍历,找到最佳的切分方式。最佳切分也就是使得切分后能达到最低误差的切分。如
果切分数据集后效果提升不够大,那么就不应进行切分操作而直接创建叶节点O。另外还需要检
查两个切分后的子集大小,如果某个子集的大小小于用户定义的参数to1N,那么也不应切分。
最后,如果这些提前终止条件都不满足,那么就返回切分特征和特征值)o

'''
def regLeaf(dataSet):#returns the value used for each leaf
    return mean(dataSet[:,-1])

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

'''
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))
'''
    从程序清单9-1可以看出,除了数据集以外,函数chooseBestSplit()还有leafType,
errType和ops这三个参数。其中leafType是对创建叶节点的函数的引用,errType是对
前面介绍的总方差计算函数的引用,而。ps是一个用户定义的参数构成的元组,用以完成树
的构建。

'''
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]):
            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
'''
通过降低决策树的复杂度来避免过拟合的过程称为剪枝(pruning )

'''
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  
'''
函数prune()的伪代码如下:
基于已有的树切分测试数据:
    如果存在任一子集是一棵树,则在该子集递归剪枝过程
    计算将当前两个叶节点合并后的误差
    计算不合并的误差
    如果合并会降低误差的话,就将叶节点合并


程序清单9-3中包含三个函数:
isTree()、getMean()和prune ( )。其中isTree()用
于测试输人变量是否是一棵树,
的节点是否是叶节点。
返回布尔类型的结果
。换句话说,该函数用于判断当前处理
函数getMean()是一个递归函数,
它从上往下遍历树直到叶节点为止。如果找到两个叶节点
则计算它们的平均值。
函数时应明确这一点。
。该函数对树进行塌陷处理(即返回树平均值),在prun
e()函数中调用该
程序清单9-3的主函数是prune ( ),
它有两个参数:待剪枝的树与剪枝所需的测试数据
testDatao prune()函数首先需要确认测试集是否为空
一旦非空,则反复递归调用函数prune()对测试数据进行切分。

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


'''
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
'''
树回归进行预测

    对于输人的单个数据点或者行向量,函数treeForeCast()会返回一个浮点值。在给定树结
构的情况下,对于单个数据点,该函数会给出一个预测值。调用函数treeForeCast()时需要指
定树的类型,以便在叶节点上能够调用合适的模型。参数modelEval是对叶节点数据进行预测
的函数的引用。函数treeForeCast()自顶向下遍历整棵树,直到命中叶节点为止。一旦到达叶
节点,它就会在输人数据上调用modelEval()函数,而该函数的默认值是regTreeEval()。
    要对回归树叶节点进行预测,就调用函数regTreeEval();要对模型树节点进行预测时,
就调用modelTreeEval()函数。它们会对输人数据进行格式化处理,在原数据矩阵上增加第。
列,然后计算并返回预测值。为了与函数modelTreeEval()保持一致,尽管regTreeEval()
只使用一个输入,但仍保留了两个输人参数。
    最后一个函数是createForCast(),它会多次调用treeForeCast()函数。由于它能够以
向量形式返回一组预测值,因此该函数在对整个测试集进行预测时非常有用。下面很快会看到这
一点。
    接下来考虑图9-6所示的数据。该数据是我从多个骑自行车的人那里收集得到的。图中给出
骑自行车的速度和人的智商之间的关系。下面将基于该数据集建立多个模型并在另一个测试集上
进行测试。对应的训练集数据保存在文件bikeSpeedVsIq train.txt中,而测试集数据保存在文件
bikeSpeedVsIq_ test.txt中。

'''    
def regTreeEval(model, inDat):
    return float(model)

def modelTreeEval(model, inDat):
    n = shape(inDat)[1]
    X = mat(ones((1,n+1)))
    X[:,1:n+1]=inDat
    return float(X*model)

def treeForeCast(tree, inData, modelEval=regTreeEval):
    if not isTree(tree): return modelEval(tree, inData)
    if inData[tree['spInd']] > tree['spVal']:
        if isTree(tree['left']): return treeForeCast(tree['left'], inData, modelEval)
        else: return modelEval(tree['left'], inData)
    else:
        if isTree(tree['right']): return treeForeCast(tree['right'], inData, modelEval)
        else: return modelEval(tree['right'], inData)
'''
创建一颗回归树
>>> trainMat = mat(regTrees.loadDataSet('bikeSpeedVsIq_train.txt'))
>>> testMat = mat(regTrees.loadDataSet('bikeSpeedVsIq_test.txt'))
>>> myTree = regTrees.createTree(trainMat,ops=(1,20))
>>> yHat = regTrees.createForeCast(myTree,testMat[:,0])
>>> corrcoef(yHat,testMat[:,1],rowvar=0)[0,1]
0.96408523182221306

创建一颗模型树
>>> myTree = regTrees.createTree(trainMat,regTrees.modelLeaf,regTrees.modelErr,(1,20))
>>> yHat = regTrees.createForeCast(myTree,testMat[:,0],regTrees.modelTreeEval)
>>> corrcoef(yHat,testMat[:,1],rowvar=0)[0,1]
0.97604121913806285

          示例:利用GUI对回归树调优
(I)收集数据:所提供的文本文件。
(2)准备数据:用Python解析上述文件,得到数值型数据。
(3)分析数据:用Tkinter构建一个GUI来展示模型和数据。
(’)训练算法:训练一裸回归树和一裸模型树,并与数据集一起展示出来。
(5)测试算法:这里不需要侧试过程。
(6)使用算法:GUIT}得人们可以在预剪枝时测试不同参数的影响,还可以帮助我们选择
    模型的类型。

'''        
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

你可能感兴趣的:(树回归(源码实现))