#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