在前面决策树的介绍中,我们使用ID3算法来构建决策树;这里我们使用CART算法来构建回归树和模型树。ID3算法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来区分。比如,如果一个特征有4种取值,那么数据将被切分成4份。很明显,该算法不适用于标签值为连续型的数据。
CART算法使用二元切分法来处理连续型变量,即每次把数据集切分成左右两份。
回归树
回归树使用CART算法来构建树,使用二元切分法来分割数据,其叶节点包含单个值。创建回归树的函数createTree()的伪代码大致如下:
找到最佳的待切分特征:
如果该节点不能再分,将该节点存为叶节点
执行二元切分
在左子树调用createTree()方法
在右子树调用createTree()方法
创建回归树的过程与决策树类似,只是切分方法不同。同时,在计算数据的无序程度时,决策树是使用香农熵的方法,而我们的标签值是连续值,不能用该方法。那么,怎么计算连续型数值的混乱度呢?首先,计算所有数据的均值,然后计算每条数据的值到均值的差值,再求平方和,即我们用总方差的方法来度量连续型数值的混乱度。在回归树中,叶节点包含单个值,所以总方差可以通过均方差乘以数据集中样本点的个数来得到。
下面,将计算连续型数值混乱度的代码提供如下:
#计算分割代价
def spilt_loss(left,right): #总方差越小,说明数据混乱度越小
loss=0.0
left_size=len(left)
#print 'left_size:',left_size
left_label=[row[-1] for row in left]
right_size=len(right)
right_label=[row[-1] for row in right]
loss += var(left_label)*left_size + var(right_label)*right_size
return loss
得到叶节点预测值的代码:
#决定输出标签(取出叶节点数据的标签值,计算平均值)
def decide_label(data):
output=[row[-1] for row in data]
return mean(output)
模型树
模型树与回归树的差别在于:回归树的叶节点是节点数据标签值的平均值,而模型树的节点数据是一个线性模型(可用最简单的最小二乘法来构建线性模型),返回线性模型的系数W,我们只要将测试数据X乘以W便可以得到预测值Y,即Y=X*W。所以该模型是由多个线性片段组成的。
同样,给出叶节点预测值及计算待分割数据集混乱度的代码:
#生成叶节点
def decide_label(dataSet):
ws,X,Y = linearModel(dataSet)
return ws
#计算模型误差
def spilt_loss(dataSet):
ws,X,Y = linearModel(dataSet)
yat = X * ws
return sum(power(yat-Y,2))
#模型预测数据
def modelTreeForecast(ws,dataRow):
data = mat(dataRow)
n = shape(data)[1]
X = mat(ones((1,n)))
X[:,1:n] = data[:,0:n-1]
return X*ws
那么,如何比较回归树与模型树那种模型更好呢?一个比较客观的方法是计算预测值与实际值相关系数。该相关系数可以通过调用NumPy库中的命令corrcoef(yHat,y.rowvar=0)来求解,其中yHat是预测值,y是目标变量的实际值。
剪枝
通过降低树的复杂度来避免过拟合的过程称为剪枝。对树的剪枝分为预剪枝和后剪枝。一般地,为了寻求最佳模型可以同时使用这两种剪枝技术。
预剪枝:在选择创建树的过程中,我们限制树的迭代次数(即限制树的深度),以及限制叶节点的样本数不要过小,设定这种提前终止条件的方法实际上就是所谓的预剪枝。周志华的西瓜书中有对预剪枝的方法做具体描述,感兴趣的同学可以了解一下。因为我只是通过提前终止条件的方法来实现预剪枝,这种方法比较简单,不做具体描述。
后剪枝:使用后剪枝方法需要将数据集分为测试集和训练集。用测试集来判断将这些叶节点合并是否能降低测试误差,如果是的话将合并。
直接上代码:
'''后剪枝过程'''
#判断是否为字典
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(testData,tree):
if len(testData)==0:
return getMean(tree)
if (isTree(tree['left']) or isTree(tree['right'])): #判断tree['left']和tree['right']是否为字典,如果为字典则进行数据划分
lSet,rSet = data_spilt(testData,tree['index'],tree['value']) #划分数据集
if isTree(tree['left']): #如果tree['left']是字典,则执行prune()函数进行递归,直到tree['left']是叶节点时结束递归,往下继续执行函数
tree['left'] = prune(lSet,tree['left'])
if isTree(tree['right']): #在tree['left']执行递归的基础上继续递归,这样可以取到所有左右两边的叶节点的值
tree['right'] = prune(lSet,tree['right'])
if not isTree(tree['left']) and not isTree(tree['right']): #如果tree['left']和tree['right']都不是字典,执行下面操作
lSet,rSet = data_spilt(testData,tree['index'],tree['value']) #分割数据集
left_value = [row[-1] for row in lSet] #取出左数据集的节点值
right_value = [row[-1] for row in rSet] #取出右数据集的节点值
if tree['left'] is None or tree['right'] is None: #如果出现tree['left']或tree['right']为None时,返回树,不执行剪枝操作
return tree
else:
errorNoMerge = sum(power(left_value-tree['left'],2)) + sum(power(right_value-tree['right'],2)) #计算没剪枝时测试集的标签值与叶节点的预测值的总方差
treeMean = (tree['left'] + tree['right'])/2.0
testSet_value = [row[-1] for row in testData]
errorMerge = sum(power(testSet_value-treeMean,2)) #计算剪枝后测试集的标签值与叶节点的预测值的总方差
if errorMerge < errorNoMerge: #如果剪枝后的方差小于剪枝前,则执行剪枝;否则返回,不剪枝。
print 'merging'
return treeMean
else:
return tree
else :
return tree
以上,便是我在学习过程中对回归树,模型树,树剪枝的一些总结。