相关博文:
《机器学习实战》学习笔记第三章 —— 决策树
主要内容:
一.CART算法简介
二.分类树
三.回归树
四.构建回归树
五.回归树的剪枝
六.模型树
七.树回归与标准回归的比较
一.CART算法简介
1.对于上一篇博客所介绍的决策树,其使用的算法是ID3算法或者是C4.5算法,他们都是根据特征的所有取值情况来进行分割结点的。也正因如此,这两种算法都只能用于离散型的特征,而不能处理连续型的特征。为了解决这个问题,我们使用二元切分法来对连续型的特征进行处理,所谓二元切分法,其实就是一个对特征进行 True or False的判断(最简单如:是或不是、小于或大于等于),这个判断就将数据分割成两半,而不管其特征是连续型的还是离散型的。显而易见,以这种方法构建出来的决策树是一棵二叉树。这就是CART算法最基本的思路。
二.分类树
1.CART算法使用在离散型特征的数据上,则称为分类树(分类还是回归不是以Y来界定的吗?为什么这里以特征X来界定?)。在这里不再像ID3算法那样使用熵来衡量数据(指的是Y)的不确定性,而是使用“基尼指数”。基尼指数的详情如下:
2.CART算法之分类树:
三.回归树
1.同样地,在CART算法之回归树中,数据的不确定性不再是用熵来衡量,但也不是用基尼指数,而是用总方差。
问1)为什么分类树中不用熵,而是用基尼指数呢?
这个问题倒真的想不到解释,感觉两种都可以用来衡量离散型变量的不确定性。待解决……
问2)为什么回归树中不用熵或者基尼指数呢?
这个很容易解释,离散型变量可以数出现的个数来计算概率,但是连续型变量,对于单单一个点的值而言,是没有概率的,所以熵或者基尼指数不能用来衡量连续型变量的不确定性。
问3)为什么使用总方差而不是方差(总方差/m)呢?
《统计学习方法》里面提到一句话“基尼指数值越大,样本集合的不确定性也越大”,其中个人觉得“样本集合”这个词是关键。所谓样本集合,一个表现特征就是规模。我想,规模也是影响“数据混乱程度”的一个因素。所以使用总方差。当然这只是个人感性的理解,并没有任何的理论参考或推敲。
2.CART算法之回归树:
四.构建回归树
1.算法流程:
2.代码及注释:
def binSplitDataSet(dataSet, feature, value): #根据分割特征及其值将数据分成两半 mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0] mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0] return mat0,mat1 '''选择最好的分割特征及其值''' def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)): tolS = ops[0]; tolN = ops[1] #tolS是分割误差减少的下限,tolN是分割后每个子树的结点个数下限 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 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) #exit cond 2 mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue) if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): #话说这一步不是已经在双重循环里面了吗? return None, leafType(dataSet) return bestIndex,bestValue def regLeaf(dataSet): #生成叶子结点,均值作为返回值(即预测值) return mean(dataSet[:,-1]) def regErr(dataSet): #计算总方差 return var(dataSet[:,-1]) * shape(dataSet)[0] '''构建回归树:leafType是建立叶子结点的函数,errType是计算误差的函数,ops是参数元组(用于预剪枝)''' 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) #选择最好的分割特征及其值 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.剪枝有预剪枝和后剪枝两种。预剪枝对设定的参数非常敏感,如上面代码中tolS和tolN两个参数,分别是分割误差减少的下限、分割后每个子树的结点个数下限。基于预剪枝的性能不太好控制,我们就应着手于后剪枝的研究,其伪代码如下:
2.代码及注释如下:
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 (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) #如果右儿子是树,则对其进行剪枝 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
六.模型树
1.上面所介绍的决策树中,所有叶子结点,也就是预测值都是直接设定为在该叶子节点上的数据的Y的均值。简而言之,就是叶子结点放的是均值,是一个确定的值。但除此之外,我们还可以在叶子节点上放一个函数,以此进行预测。
2.例如,我们可以在叶子结点上放一个线性回归模型,也正因如此,前面代码中生成叶子结点的方式以及计算总方差的方式方式了改变。详情如下:
'''线性回归模型''' def linearSolve(dataSet): #利用最小二乘法计算线性回归模型的参数ws 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):#返回线性回归模型的参数ws ws,X,Y = linearSolve(dataSet) return ws def modelErr(dataSet): #计算总方差,因而Y值是X的函数,而不再是之前的均值,所以不能利用以前的误差计算方法 ws,X,Y = linearSolve(dataSet) yHat = X * ws return sum(power(Y - yHat,2))
七.回归树、模型树、线性回归的比较
既然介绍了回归树与模型树,且模型树又用到了线性回归模型,且三者都能对同样的数据进行预测,那就理所当然地对它们作出一些比较,分出优劣。具体实现实现如下:
'''普通回归树叶子结点返回均值。为什么要这个没有用的inData?其实只是为了方便统一输入参数,因为模型树需要输入参数''' def regTreeEval(model,inData ): return float(model) def modelTreeEval(model, inDat): #模型树叶子结点对测试数据的预测值, 与上面的regTreeEval()函数是同类型 n = shape(inDat)[1] X = mat(ones((1,n+1))) X[:,1:n+1]=inDat return float(X*model) '''modelEval是计算叶子结点的值的函数,可以是regLeaf()对应普通回归树,可以是modelTreeEval()对应模型树''' def treeForeCast(tree, inData, modelEval=regTreeEval): #搜索回归树,找到合适的预测值. if not isTree(tree): return modelEval(tree, inData) if inData[tree['spInd']] > tree['spVal']: return treeForeCast(tree['left'], inData, modelEval) else: return treeForeCast(tree['right'], inData, modelEval) 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
然后测试一下三者对测试数据的预测效果,这里用相关系数R2来衡量,R2的值越接近于1.0,预测的效果越好。
首先是回归树:
然后是模型树:
最后是线性回归:
从上面可以看得出:模型树 > 回归树 > 线性回归 。
所以多做了点功夫,效果就较之好一点,是说得过去的。