树回归CART概述
线性回归创建的模型需要拟合所有的样本点(局部加权线性回归除外)。当数据拥有众多特征并且特征之间关系十分复杂时,构建全局模型的想法就显得太难了,也略显笨拙。而且,实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。一种可行的方法是将数据集切分成很多份易建模的数据,然后利用我们的线性回归技术来建模。如果首次切分后仍然难以拟合线性模型就继续切分。在这种切分方式下,树回归和回归法就相当有用。
如何计算连续型数值的混乱度:计算连续型数值的混乱度是非常简单的。首先计算所有数据的均值,然后计算每条数据的值到均值的差值。为了对正负差值同等看待,一般使用绝对值或平方值来代替上述差值。这种做法有点类似于前面介绍过的统计学中常用的方差计算。唯一不同就是,方差是平方误差的均值(均方差),而这里需要的是平方误差的总值(总方差)。总方差可以通过均方差乘以数据集中样本点的个数来得到。
CART(Classification And Regression Trees, 分类回归树) :可以用于分类还可以用于回归。树构建算法ID3 的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。切分过于迅速外,它不能直接处理连续型特征。CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量,对 CART 稍作修改就可以处理回归问题。
CART算法
分类与回归树(classification and regression tree ,CART)可以用于分类与回归。ID3 和C4.5 树都是多叉结构,CART树是二叉树,内部节点特征的取值为“是”和“否”,左分支是取值为“是”的分支,右分支是取值为“否”的分支。
回归树的生成:一个回归树对应着输入空间(特征空间)的一个划分以及在划分的单元上的输出值。假设已将输入空间划分为M个单元R1,R2,⋯,RM,并在每个单元Rm上有一个固定的输出值,于是回归树模型可表示为:
当输入空间的划分确定时,可以用平方误差来表示回归树对于训练数据的预测误差:
下面算法第一步中公式里面的c1和c2是用求得的切分点j和s划分的两个数据集并求其平均值。
分类树的生成
基尼指数:分类问题中,假设有K个类,样本点属于第k类的概率为pk,则概率分布的基尼指数定义为:
对于给定的样本集合D,其基尼指数为:
概率分布的基尼指数与熵意义类似,值越大,其不确定性越大。
如果样本集合D根据特征A是否取某一可能值a被分割成D1和D2两部分,则在特征A的条件下,集合D的基尼指数定义为:
Gini(D)是计算样本集的基尼指数,表示集合D的不确定性;
Gini(D,A)是表示经A=a分割后集合D的不确定性,选择时要选择最小的,和H(D|A)意义类似,都是取最小的。H(D|A)是对特征做计算,选择一个特征,Gini(D,A)是对特征及其一个取值做计算,选择一个特征及其分割点。
注意:ID3和C4.5建树时是选取特征,切分点自动根据特征取值确定;CART分类树建树时是选取特征及其切分点。
建树的过程和ID3、C4.5类似,三个结束条件:1.无更多特征(建树时下层的节点仍然会有可能用到上层特征,但不可能是同一切分点);2.节点中的样本数小于预定的阈值;3.样本集的基尼指数大于指定阈值。
数据处理
按指定某个值切分矩阵
def binSplitDataSet(dataSet, feature, value):
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
return mat0,mat1
'''
>>> testMat = mat(eye(4))
>>> testMat[:,1] > 0.5
matrix([[False],[ True],[False],[False]], dtype=bool)
>>> nonzero(testMat[:,1] > 0.5)
(array([1], dtype=int64), array([0], dtype=int64))
>>> nonzero(testMat[:,1] > 0.5)[0]
array([1], dtype=int64)
'''
def regLeaf(dataSet):
# 负责生成叶节点,当不在对数据进行切分时,调用该函数生成叶节点模型,在回归树中,该模型就是目标变量的均值
return mean(dataSet[:,-1])
def regErr(dataSet):
return var(dataSet[:,-1]) * shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
'''用最佳方式切分数据集 和 生成相应的叶节点
对每个特征:
对每个特征值:
将数据集切分成两份(小于该特征值的数据样本放在左子树,否则放在右子树)
计算切分的误差
如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
返回最佳切分的特征和阈值
:param dataSet:加载的原始数据集
:param leafType:建立叶子点的函数
:param errType:误差计算函数(求总方差)
:param ops:[容许误差下降值,切分的最少样本数]。
:return:
bestIndex:bestIndex feature的index坐标
bestValue:bestValue 切分的最优值
'''
tolS,tolN = ops[0],ops[1]
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: # matrix([[ 0.],[ 1.],[ 0.],[ 0.]]).T.tolist() --> [[0.0, 1.0, 0.0, 0.0]]
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].T.tolist()[0]): # [0]表示这一列的[所有行],不要[0]就是一个array[[所有行]]
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)):
'''获取回归树
:param dataSet:加载的原始数据集
:param leafType:建立叶子点的函数
:param errType:误差计算函数
:param ops:容许误差下降值,切分的最少样本数
:return:
retTree:决策树最后的结果
'''
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
树减枝
一棵树如果节点过多,表明该模型可能对数据进行了 “过拟合”。通过降低决策树的复杂度来避免过拟合的过程称为 剪枝(pruning)。在函数 chooseBestSplit() 中提前终止条件,实际上是在进行一种所谓的 预剪枝(prepruning)操作。另一个形式的剪枝需要使用测试集和训练集,称作 后剪枝(postpruning)。预剪枝就是及早的停止树增长,在构造决策树的同时进行剪枝。所有决策树的构建方法,都是在无法进一步降低熵的情况下才会停止创建分支的过程,为了避免过拟合,可以设定一个阈值,熵减小的数量小于这个阈值,即使还可以继续降低熵,也停止继续创建分支。但是这种方法实际中的效果并不好。后剪枝就是决策树构造完成后进行剪枝。剪枝的过程是对拥有同样父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否小于某一阈值。如果确实小,则这一组节点可以合并一个节点,其中包含了所有可能的结果。合并也被称作 塌陷处理 ,在回归树中一般采用取需要合并的所有子树的平均值。后剪枝是目前最普遍的做法。
基于已有的树切分测试数据:
如果存在任一子集是一棵树,则在该子集递归剪枝过程
计算将当前两个叶节点合并后的误差
计算不合并的误差
如果合并会降低误差的话,就将叶节点合并
后剪枝
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):
'''
:param tree:待剪枝的树
:param testData:剪枝所需要的测试数据 testData
:return:
tree:剪枝完成的树
'''
if shape(testData)[0] == 0: return getMean(tree) # 判断是否测试数据集没有数据,如果没有,就直接返回tree本身的均值(塌陷处理)
if (isTree(tree['right']) or isTree(tree['left'])): # 判断分枝是否是dict字典,如果是就将测试数据集进行切分
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: # 注意返回的结果: 如果可以合并,原来的dict就变为了 数值
print "merging"
return treeMean
else: return tree
else: return tree
后剪枝可能不如预剪纸有效,一般为求最大模型可以同时使用两种剪枝技术