当数据拥有众多特征且特征之间的关系十分复杂时,构建全局的模型就太难了。实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。
def loadDataSet(fileName):
fr = open(fileName)
dataMat = []
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine)
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):
'''数据集,待切分的特征,该特征的某个值'''
# np.nonzero函数用于得到数组array中非零元素的位置(数组索引)
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
# 将集合切分成两个子集并返回
return mat0,mat1
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
'''数据集,建立叶节点的函数,误差计算函数,包含构建树所需的其他参数的元组'''
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. 复杂数据的局部建模
决策树不断将数据切分成小数据集,知道目标变量完全相同,或则数据不能再切分为止。决策树是一种贪心算法,它要在给定的时间内作出最佳选择而不管能否达到全局最优。
CART是一个使用很广泛的树构建算法,它使用二元切分来处理连续变量。对CART稍作修改就可以处理回归问题。下面将实现CART算法和回归树。回归树与分类树思路相似,但叶节点的数据类型不是散型而是连续型。
2.连续和离散型特征的树的构建
在树的构建中,需要解决多种类型数据的存储问题。将使用字典来存储树的数据结构,该字典包含以下四个元素:
1. 待切分的特征
2. 待切分的特征值
3. 右子树
4. 左子树
CART算法只做二元切分,所以这里可以固定树的数据结构。树包含左键和右键,可以存储另一棵子树或则单个值。字典还包括特征和特征值。可以用下面的代码来建立树节点:
class treeNode():
def __init__(self, feat, val, right, left):
featureToSplitOn = feat
valueOfSplit = val
rightBranch = right
leftBranch = left
本章可以构建两种树:第一种是回归树,其叶节点包含单个值;第二种是模型树,其每个节点包含一个线性方程。创建这两种树时,我们尽量使得代码之间可以重用。
CART树实现代码:
def loadDataSet(fileName):
fr = open(fileName)
dataMat = []
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine)
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):
'''数据集,待切分的特征,该特征的某个值'''
# np.nonzero函数用于得到数组array中非零元素的位置(数组索引)
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
# 将集合切分成两个子集并返回
return mat0,mat1
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
'''数据集,建立叶节点的函数,误差计算函数,包含构建树所需的其他参数的元组'''
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
函数createTree()是一个递归函数,该函数首先将数据集分成两个部分,切分由函数chooseBestSplit()完成,这里还未给出该函数的实现。如归满足停止条件,该函数将会返回None和某类模型的值。如果构建的是回归树,该模型是一个常数。如果是模型树,其模型是一个线性方程。
我们运行一下按指定列的某个值来切分矩阵的程序的结果:
下面给出回归树的chooseBestSplit()函数。针对回归树的构建来给chooseBestSplit()函数加入具体的代码。
3.将CART算法用于回归
为成功构建以分段常数为叶节点的树,需要度量出数据的一致性。之前我们使用树进行分类时,会给定节点时计算数据的混乱度。那么如何计算连续型数值的混乱度呢?首先计算所有数据的均值,然后计算每条数据的值到均值的差值。为了对正负差值同等看待,一般使用绝对值或平方值代替上述差值。这里需要的是平方误差的总值(总方差)。总方差可以通过均方差乘以数据集中样本点个数来得到。
3.1 构建树
下面给出上述三个函数的具体实现代码:
def regLeaf(dataSet):
'''生成叶节点 得到叶节点的模型。在回归树中该模型就是目标变量的均值'''
return mean(dataSet[:,-1])
def regErr(dataSet):
'''在给定数据上计算目标变量的平方误差
var()均方差函数 乘以 数据集中的样本个数
'''
return var(dataSet[:-1]) * shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
#切分特征的参数阈值,用户初始设置好
tolS = ops[0] #允许的误差下降值
tolN = ops[1] #切分的最小样本数
#若所有特征值都相同,停止切分
if len(set(dataSet[:,-1].T.tolist()[0])) == 1:#倒数第一列转化成list 不重复
return None,leafType(dataSet) #如果剩余特征数为1,停止切分1。
# 找不到好的切分特征,调用regLeaf直接生成叶结点
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]): python3报错修改为下面
for splitVal in set((dataSet[:, featIndex].T.A.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) #停止切分2
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
#判断切分后子集大小,小于最小允许样本数停止切分3
if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
return None, leafType(dataSet)
return bestIndex,bestValue#返回特征编号和用于切分的特征值
3.2 运行代码
4. 树剪枝
一棵树如果节点过多,表明该模型可能对数据进行了‘过拟合’。前面章节使用了在测试集上某种交叉验证技术来发现过拟合,决策树亦是如此。
通过降低决策树的复杂度来避免过拟合的过程称为剪枝。其实之前我们已经进行过剪枝了。在函数 chooseBestSplit中的提前终止条件,实际上一种所谓的预剪枝操作。另一种剪枝操作需要训练集和测试集,称作后剪枝。
4.1 预剪枝
上节的两个实验的结果背后存在一些问题。树构建算法对输入的参数tols和toln十分敏感,使用其他值不太容易达到这么好的效果。...
与之前只有两个节点的树相比,这里的太过臃肿,它甚至为了数据集中每个样本都分配了一个节点。
...
图9-3中的散点图与9-1十分相似,但是数量级差了100倍。而9-1的图分出来树只有两个节点,而这里构建出的节点有很多节点。产生这个现象的原因在于,停止条件tols对误差的数量级十分敏感。如果在选项中花费时间并对上述误差的容忍取平方值,或许也能得到仅有两个叶节点组成的树:
然而,通过不断修改停止条件来得到合理结果并不是一个很好的办法。下面将讨论后剪枝,是一个更理想的剪枝方法。
4.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 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
这里假设发生了过拟合,从而对树进行剪枝。
接下来检查某个分支到底是子树还是节点。如果是子树,就调用pure来对该子树进行剪枝。对左右两个分支剪完之后,还需要检查他们是否仍还是子树。如果两个分支已经不再是子树,那么就可以合并。具体做法是对合并前后的误差进行比较。如果合并之后的误差比不合并的误差小就进行合并操作。
可以看到,大量的节点已经被剪掉了,但是没有预期的那样剪枝成两个部分,说明后剪枝可能不如预剪枝有效。一般来说,为了寻求最佳模型可以同时使用两个剪枝技术。