在别人那里摘过来的,并添加自已的看法。 CART(classification and regression trees,分类回归树)
之前使用过的分类树构建算法是ID3,ID3决策树学习算法是以信息增益为准则来选择划分属性。ID3的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有4种取值,那么数据将被切成4份。一旦按某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以所以有观点认为这种切分方式过于迅速。另外一种方法是二元切分法,即每次把数据集切成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入树的右子树。
ID3算法还存在另一个问题,它不能直接处理连续性数据。只有事先将连续特征转换成离散型,才能在ID3算法中使用。
CART算法使用二元切分来处理连续型变量。对CART稍作修改就可以处理回归问题。CART决策树使用“基尼指数”来选择划分属性,基尼值是用来度量数据集的纯度。 #####分类树 以C4.5分类树为例,C4.5分类树在每次分枝时,是穷举每一个feature的每一个阈值,找到使得按照feature<=阈值,和feature>阈值分成的两个分枝的熵最大的阈值(熵最大的概念可理解成尽可能每个分枝的男女比例都远离1:1),按照该标准分枝得到两个新节点,用同样方法继续分枝直到所有人都被分入性别唯一的叶子节点,或达到预设的终止条件,若最终叶子节点中的性别不唯一,则以多数人的性别作为该叶子节点的性别。 总结:分类树使用信息增益或增益比率来划分节点;每个节点样本的类别情况投票决定测试样本的类别。
#####回归树 回归树总体流程也是类似,区别在于,回归树的每个节点(不一定是叶子节点)都会得一个预测值,以年龄为例,该预测值等于属于这个节点的所有人年龄的平均值。分枝时穷举每一个feature的每个阈值找最好的分割点,但衡量最好的标准不再是最大熵,而是最小化均方差即(每个人的年龄-预测年龄)^2 的总和 / N。也就是被预测出错的人数越多,错的越离谱,均方差就越大,通过最小化均方差能够找到最可靠的分枝依据。分枝直到每个叶子节点上人的年龄都唯一或者达到预设的终止条件(如叶子个数上限),若最终叶子节点上人的年龄不唯一,则以该节点上所有人的平均年龄做为该叶子节点的预测年龄。 总结:回归树使用最大均方差划分节点;每个节点样本的均值作为测试样本的回归预测值。 决策树是一种贪心算法,它要在给定时间内做出最佳选择,但并不关心能否达到全局最优。
CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续性变量。对于CART 稍作修改就可以处理回归问题。
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 # dataSet: 数据集合 # feature: 待切分的特征 # value: 该特征的某个值 def binSplitDataSet(dataSet, feature, value): mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:] print dataSet[:,feature] > value print nonzero(dataSet[:,feature] > value)[0] print dataSet[nonzero(dataSet[:, feature] > value)[0], :] mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:] print '==========' print dataSet[:, feature] <= value print nonzero(dataSet[:, feature] <= value)[0] print dataSet[nonzero(dataSet[:, feature] <= value)[0], :] return mat0,mat1 '''在这里做个测试,验证binSplitDataSet()将数据集切分成两个子集返回''' def test1(): testMat = mat(eye(4)) print testMat mat0, mat1 = binSplitDataSet(testMat, 1, 0.5) print '============' print mat0 print mat1 test1()结果:
[[ 1. 0. 0. 0.] [ 0. 1. 0. 0.] [ 0. 0. 1. 0.] [ 0. 0. 0. 1.]] [[False] [ True] [False] [False]] [1] [[ 0. 1. 0. 0.]] ========== [[ True] [False] [ True] [ True]] [0 2 3] [[ 1. 0. 0. 0.] [ 0. 0. 1. 0.] [ 0. 0. 0. 1.]] ============ [[ 0. 1. 0. 0.]] [[ 1. 0. 0. 0.] [ 0. 0. 1. 0.] [ 0. 0. 0. 1.]]构建树首先要实现如何切分数据集,使用函数chooseBestSplit()函数切分数据集。给定误差计算方法,该函数寻找数据集上的最佳二元切分方式,一旦停止切分会生成一个叶节点。它遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。 函数的伪代码如下:
对每个特征 : 对每个特征值 : 将数据集切分成两份 计算切分的误差 如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差 返回最佳切分的特征和阈值 # 负责生成叶节点,当chooseBestSplit()函数确定不再对数据进行切分时, # 将调用该regLeaf()函数来得到叶节点的模型,在回归树中,该模型其实就是目标变量的均值 def regLeaf(dataSet):#returns the value used for each leaf return mean(dataSet[:,-1]) # 误差估计函数,该函数在给定的数据上计算目标变量的平方误差,这里直接调用均方差函数var # 因为这里需要返回的是总方差,所以要用均方差乘以数据集中样本的个数 def regErr(dataSet): return var(dataSet[:,-1]) * shape(dataSet)[0] 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)) # 回归树的切分函数,构建回归树的核心函数。目的:找出数据的最佳二元切分方式。如果找不到 # 一个“好”的二元切分,该函数返回None并同时调用createTree()方法来产生叶节点,叶节点的 # 值也将返回None。 # 如果找到一个“好”的切分方式,则返回特征编号和切分特征值。 # 最佳切分就是使得切分后能达到最低误差的切分。 def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)): # tolS是容许的误差下降值 # tolN是切分的最小样本数 tolS = ops[0]; tolN = ops[1] '''把 np 类型矩阵的一列转换成行,然后转换成 list 类型,取这一行有多少元素''' # 如果剩余特征值的数目为1,那么就不再切分而返回 if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #exit cond 1 return None, leafType(dataSet) # 当前数据集的大小 m,n = shape(dataSet) # 当前数据集的误差 # 计算数据集最后一列的特征总方差。 S = errType(dataSet) '''float("inf") 表示正负无穷''' bestS = inf; bestIndex = 0; bestValue = 0 for featIndex in range(n-1): for splitVal in set(dataSet[:,featIndex].T.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) #exit cond 2 mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue) # 检查切分后的子集大小,如果某个子集的大小小于用户定义的参数tolN,那么也不应切分。 if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): #exit cond 3 return None, leafType(dataSet) # 如果前面的这些终止条件都不满足,那么就返回切分特征和特征值。 return bestIndex,bestValue # dataSet: 数据集合 # leafType: 给出建立叶节点的函数 # errType: 误差计算函数 # ops: 包含树构建所需其他参数的元组 def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)): # 将数据集分成两个部分,若满足停止条件,chooseBestSplit将返回None和某类模型的值 # 若构建的是回归树,该模型是个常数。若是模型树,其模型是一个线性方程。 # 若不满足停止条件,chooseBestSplit()将创建一个新的Python字典,并将数据集分成两份, # 在这两份数据集上将分别继续递归调用createTree()函数 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.首先使用chooseBestSplit()函数,回归树的切分函数,构建回归树的核心函数。目的:找出数据的最佳二元切分方式。 如果找不到一个“好”的二元切分,该函数返回None并同时调用createTree()方法来产生叶节点,叶节点的值也将返回None。如果找到一个“好”的切分方式,则返回特征编号和切分特征值。最佳切分就是使得切分后能达到最低误差的切分。
细说代码:
传入一个数据矩阵、生成叶子结点的函数、计算总方差的函数、一个自定义参数。 2. 一切都是先计算数据最后一列的总方差(第二列的特征),然后搞一个for循环(几个特征,实际上只有一个循环,第一个列特征). 3. 下面包含一个循环,是关于。。把第一个特征所有的数值去重,得到的数值作为一个循环的次数,每次拿出一个值,并根据这个值传入到binSplitDataSet(),根据这个特征值把数据集分成两部分。 4. 加了一个IF 条件,防止回归树过拟合,判断划分成树叶子结点内小于4就被认为不在分裂。 5. 计算分裂后依据第一个特征分裂两个空间,分别计算两个空间的方差再相加,与总方差相比较取最小,类似在列表中找最小值。循环往复,找到好的分割点且属于哪一个特。一棵树如果节点过多,表示该模型可能对数据进行了“过拟合”。通过降低决策树的复杂度来避免过拟合的过程称为剪枝(pruning)。
剪枝分为预剪枝(prepruning)和后剪枝(postpruning)。
预剪枝是指在决策树生成过程中,对每个节点在划分前先进行估计,若当前节点的划分不能带来决策树泛化性能的提升,则停止划分并将当前节点记为叶节点(上面的程序已经使用了预剪枝);
后剪枝则是先在训练集生成一棵完整的决策树,然后自底向上地对非叶节点进行考察,若将该节点对应的子树替换为叶节点能带来决策树泛化性能提升,则将该子树替换为叶节点。 ####树剪枝: 伪代码 基于已有的树切分测试数据:
如果存在任一子集是一棵树,则在该子集地柜剪枝过程计算将当前两个叶节点合并后的误差计算不合并的误差如果合并的误差会降低的话,就将叶节点合并使用后剪枝方法需要将数据集分成测试集和训练集。首先指定参数,使得构建出的树足够大、足够复杂,便于剪枝。接下来从上而上找到叶节点,用测试集来判断将这些叶节点合并是够能降低测试误差。如果是的话就进行合并。
numpy.power(x1, x2) 数组的元素分别求n次方。x2可以是数字,也可以是数组,但是x1和x2的列数要相同。 #####总结: 盗图:https://blog.csdn.net/qq_34352010/article/details/52136209
就是说2,4,5的方差和大于x的方差,则剪枝。
或者4,5 的方差大于3 的,就可把4,5剪掉。 回归树:真的费脑筋啊,剪枝处理,首先构建一个树枝比较多的回归树,利于修剪哈,目的是为了防止树分的太细过拟合。
首先用测试数据来帮助回归树剪枝,如果发现测试集空的啦,就没必要往下分的必要啦,求个平均数得啦。继续往下匹配,发现往下分的时候,这个根节点有左右子树(有一个也是有),根据此节点分割数据,分成两份后,再去回归剪枝处理。如果发现一个是叶子结点一个子树,也是分割数据,只是相应处理子树,叶子不处理,返回原有的树。当遇到两个点都是叶子节点的时候,也是要切割数据的嘛,把数据分到两个叶子结点上去计算没有合并的误差,也计算合并的误差,比较一下,如果合并好的误差小,就剪枝处理,返回一个值作为根节点嘛。否则,返回原来的树嘛。回归树的剪枝处理就是从根节点往下去迭代,再从叶子结点处理完,一层一层返回根节点处理。当然除了剪枝技术还可以使用随机森林去解决这个过拟合问题,投票解决,公正点。
'''回归树剪枝函数''' #该函数用于判断当前处理的是否是叶节点 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): ''' Desc: 从上而下找到叶节点,用测试数据集来判断将这些叶节点合并是否能降低测试误差 Args: tree -- 待剪枝的树 testData -- 剪枝所需要的测试数据 testData Returns: tree -- 剪枝完成的树 ''' # 判断是否测试数据集没有数据,如果没有,就直接返回tree本身的均值 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['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(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 test3(): myDat2 = loadDataSet(r'\Ch09\ex2.txt') myMat2 = mat(myDat2) myTree = createTree(myMat2, ops=(0,1)) myDatTest = loadDataSet(r'E:\bookFiles\machinelearninginaction\Ch09\ex2test.txt') myDatTest = mat(myDatTest) print prune(myTree, myDatTest) test3()根据归回树的基础上,叶子节点是常数值,把这些叶子结点的设定为分段线型函数,这里的所谓的分段线性是指模型有多个线性片段组成。 该算法的关键在于误差的计算: 怎么找到最佳切分点,应该怎样计算误差,首先对于给定的数据集,应该先用线性的模型对它拟合,然后计算真实目标值和模型预测值的差值,最后将这些差值的平方和得到所需的误差。 注:线性回归(最小二乘) 通过最小化平方误差,求解回归系数w。即平方误差对w求导=0,解w。
w=(X.T * X).I * X.T *Y代码补充:
######叶子节点为线性模型的模型树######### # 线性模型 def linearSolve(dataSet): #helper function used in two places m,n = shape(dataSet) # 样本数据集合 X = mat(ones((m,n))) # 自变量 Y = mat(ones((m,1)))# 目标变量 # 样本数据集合; 标签 X[:,1:n] = dataSet[:,0:n-1]; Y = dataSet[:,-1]#and strip out Y xTx = X.T*X # 判断是否为奇异矩阵 # 若等于0,称矩阵A为奇异矩阵 ;非奇异矩阵是可逆的。 if linalg.det(xTx) == 0.0: raise NameError('行列式值为零,不能计算逆矩阵,可适当增加ops的第二个值') ws = xTx.I * (X.T * Y) return ws,X,Y 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 return sum(power(Y - yHat,2)) def test4(): myDat2 = loadDataSet(r'\Ch09\exp2.txt') myMat2 = mat(myDat2) myTree = createTree(myMat2, leafType=modelLeaf, errType=modelErr, ops=(1,10)) print myTree #test4()结果显示:
{'spInd': 0, 'spVal': 0.285477, 'right': matrix([[ 3.46877936], [ 1.18521743]]), 'left': matrix([[ 1.69855694e-03], [ 1.19647739e+01]])}可以看出,改代码以0.285477为界创建了两个模型,下图数据实际在0.3处分段。createTree() 生成的两个线性模型分别是 y = 3.468 + 1.1852 *x 和 y = 0.0016985+ 11.96477 *x 与用于生成该树的真实模型非常相近。
结果显示:
{'spInd': 0, 'spVal': 10.0, 'right': {'spInd': 0, 'spVal': 7.0, 'right': {'spInd': 0, 'spVal': 5.0, 'right': 50.946836650000002, 'left': 69.02117757692308}, 'left': 94.706657812499998}, 'left': {'spInd': 0, 'spVal': 17.0, 'right': {'spInd': 0, 'spVal': 14.0, 'right': 122.90893026923078, 'left': 141.06067981481482}, 'left': {'spInd': 0, 'spVal': 20.0, 'right': 157.04840788461539, 'left': 168.34161286956524}}} {'spInd': 0, 'spVal': 4.0, 'right': matrix([[ 68.87014372], [-11.78556471]]), 'left': {'spInd': 0, 'spVal': 12.0, 'right': {'spInd': 0, 'spVal': 9.0, 'right': {'spInd': 0, 'spVal': 6.0, 'right': matrix([[-17.21714265], [ 13.72153115]]), 'left': matrix([[-11.84548851], [ 12.12382261]])}, 'left': matrix([[ -2.87684083], [ 10.20804482]])}, 'left': {'spInd': 0, 'spVal': 16.0, 'right': matrix([[ 43.41251481], [ 6.37966738]]), 'left': {'spInd': 0, 'spVal': 20.0, 'right': matrix([[ 37.54851927], [ 6.23298637]]), 'left': matrix([[ 47.58621512], [ 5.51066299]])}}}} 普通回归树 预测结果的相关系数R2: 0.964085 模型回归树 预测结果的相关系数R2: 0.976041 模型回归树效果优于普通回归树参考文献: 机器学习实战 第九章回归树错误 Regression Tree 回归树 https://blog.csdn.net/chinachenyyx/article/details/78181331 https://blog.csdn.net/u011629133/article/details/52356012 https://blog.csdn.net/accumulate_zhang/article/details/63252641 http://www.jb51.net/article/131071.htm https://blog.csdn.net/u010454729/article/details/48929263 剪枝算法 涉及到c4.5和gini 算法 代价复杂度剪枝
