一、分类树构建存在的问题
1. 切分过于迅速
假定当前叶子节点选择特征A来分割数据,那么数据A将不再后续的叶子节点中起作用,这样就会造成切分过于迅速
2. 不能处理连续数据
想要处理连续型数据,必须先将连续性数据转化成离散型数据。CART就是特别有名的利用二分法来处理连续性变量的树形算法。将度量方法稍作修改,就可以实现回归树。
二、混乱度
分类树中我们用信息熵和信息增益来决定最优划分属性,实际上信息增益的意义就是,选择这个划分属性能够达相对最好的分类效果,也就是给定节点时区计算数据的混乱度。
对于连续型数据来说,混乱度的衡量方法很简单,计算所有数据的均值,统计出所有数据到均值点的差值,为了将正负同等看待,一般使用平方值或者是绝对值来代替。
三、伪代码
for 遍历 特征
for 遍历 特征值(我们可以选择连续两个数的平均值)
将数据切分成两部分
计算切分后的误差(混乱度)
if 判断 误差和历史最小误差比较
该误差较小 存储特征和特征值并更新历史最小误差
if 判断 误差减少较少 or 数据可供分割项较少(当误差设定为0,可供分割项选1的时候,可以生成最大树)
结束回归树构建
结束所有遍历,返回最佳特征和特征值(切分目标特征和阈值)
四、数值划分代码
#-*-coding:utf-8-*- ## 引入numpy包 from numpy import * ## 加载数据 def loadDataSet(fileName): # 按照数据应该是3 numFeat=len(open(fileName).readline().split('\t')) dataMat=[] fr=open(fileName) # for line in fr.readlines(): # curLine=line.strip().split('\t') # # map(float,curLine)用来将每行的内容保存成一组浮点数,map形式 # fltLine=map(float,curLine) # dataMat.append(fltLine) # return dataMat for line in fr.readlines(): lineArr=[] curLine=line.strip().split('\t') for i in range(numFeat): lineArr.append(float(curLine[i])) dataMat.append(lineArr) return dataMat ## 通过数组过滤的方式来集合且分为两个子集合 参数:数据集合,待切分的特征,该特征的某个值 def binSplitDataSet(dataSet,feature,value): # 大于小于得到的是一个布尔值,nonzero是根据布尔值得到相应的坐标,[0]指的是横坐标,仔细想想就知道了 mat0=dataSet[nonzero(dataSet[:,feature]>value)[0],:] mat1=dataSet[nonzero(dataSet[:,feature]<=value)[0],:] return mat0,mat1 ## 均值函数和方差函数mean&var # 叶子节点的确定 其实简单来说就是求均值嘛 def regLeaf(dataSet): return mean(dataSet[:,-1]) # 计算目标变量的平方误差,因为这里用到的是总误差,所以乘上了样本个数 def regErr(dataSet): return var(dataSet[:,-1])*shape(dataSet)[0] ## 用最佳方式分割数据集,并生成相应的叶子节点 # 第一个返回值为None的时候不会再进行切分 def chooseBestSplit(dataSet,leafType=regLeaf,errType=regErr,ops=(0,4)): # 用户指定 控制函数停止时机 (容许的误差下降值,切分的最少样本数) tolS=ops[0] tolN=ops[1] # 如果目标变量是相同值,则返回变量 if len(set(dataSet[:,-1].T.tolist()[0]))==1: print ("aaa") print (len(set(dataSet[:,-1].T.tolist()[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]): # 调用函数binSplitDataSet 将数据按照上一层结果进行简单分类 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 # 根据运行结果找到合适的叶子节点 feat & val # 当S和bestS之间差距满足最小下降值 则不再进行切分 if (S-bestS)<tolS: print ("bbb") print (S,bestS,tolS) return None,leafType(dataSet) # 当两边的切分样本 有一方小于4个 则不再进行切分 if(shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN): print ("ccc") print (shape(mat0)[0]) print (shape(mat1)[0]) print (tolN) return None,leafType(dataSet) return bestIndex,bestValue ## 创建树形结构 参数(数据集,创建叶子节点的函数,计算误差的函数,(用户定义)树构建所需元祖) def createTree(dataSet,leafType=regLeaf,errType=regErr,ops=(0,4)): feat,val=chooseBestSplit(dataSet,leafType,errType,ops) # 直到无法区分为止 if feat==None: return val # 定义返回值--二叉树 retTree={} # 定义分支节点的属性和值 retTree['spInd']=feat retTree['spVal']=val # 用binSplitDataSet将数据分割成两个部分 lSet,rSet=binSplitDataSet(dataSet,feat,val) # 递归方法,重复创建二叉树,直到无法区分位置 retTree['left']=createTree(lSet,leafType,errType,ops) retTree['right']=createTree(rSet,leafType,errType,ops) return retTree
五、剪枝
为了解决过多的复杂度而带来的过拟合问题,预剪枝和后剪枝的存在可以极大地避免决策树陷入过拟合。
离散数据(连续数据)的预剪枝和后剪枝和标称型数据的剪枝处理不太一样,因为标称型的数据分类绝对会到达分支,并且存在错误节点。但是由于离散数据在某个节点的某个特征分割之后,还可以在这个特征进一步分割,导致永远的划分都是正确的,永远都不会停止,所以它的预剪枝和后剪枝并不一样。
离散数据的预剪枝可以采用终止条件,这个终止条件可以是判断误差、也可以是判断当前节点达到什么程度,都可以。但是值得注意的是,当当前所有结点都是同一类数据的时候,直接跳出就成,没必要进行比较哟。
因为每次划分都会比原先的准确率更高,所以后剪枝通常伴随着训练集和测试集来进行,通过测试机将训练集训练出来的足够大的树进行剪枝。从上置下的寻找叶节点(但实际上,程序运行时从下向上进行合并),如果合并后会降低误差,那么就将叶节点进行合并。
六、后剪枝代码(相对多出的代码)
## 利用字典属性来判断这个有没有子节点 def isTree(obj): # 是的话,就返回true,表明这个是个有叶子节点的节点 return (type(obj).__name__=='dict') ## 递归方式获取当前值 def getMean(tree): if isTree(tree['left']): tree['left']=getMean(tree['left']) if isTree(tree['right']): tree['right']=getMean(tree['right']) return (tree['left']+tree['right'])/2.0 ## 剪枝处理 def prune(tree, testData): # 如果test的目标值为0,那么根据数结构,获取左右数据 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']) # 合并后的节点存储值 treeMean=(tree['left']+tree['right'])/2.0 # 重新生成合并前的和合并后的误差值,进行判断 errorNoMerge=sum(power(lSet[:,-1]-tree['left'],2))+sum(power(rSet[:,-1]-tree['right'],2)) errorMerge=sum(power(testData[:,-1]-treeMean,2)) # if errorMerge<errorNoMerge: print "merging" return treeMean else: return tree else: return tree
七、模型树
回归树就是之前提到的用节点来进行划分,模型树是用线性函数来进行划分。回归树的叶节点是节点数据标签值的平均值,而模型树的节点数据是一个线性模型(可用最简单的最小二乘法来构建线性模型)。
把一个叶节点定义为线性函数能够更容易的将树型结构缩减,其解释性也是由于回归树的特点之一。
回归树种用的是均值记录叶子节点信息,方差和记录误差。在模型树种使用不一样的概念。
## 线性回归解决器
def linearSolve(dataSet): m,n=shape(dataSet) # 初始化矩阵,用于简单的线性回归
X=mat(ones((m,n))) Y=mat(ones((m,1))) X[:,1:n]=dataSet[:,0:n-1] Y=dataSet[:,-1] xTx=X.T*X # 矩阵的逆不存在会造成异常
if linalg.det(xTx)==0.0: raise NameError("abc") ws=xTx.I*(X.T*Y) return ws,X,Y def modelLeaf(dataSet): ws,X,Y=linearSolve(dataSet) return ws def modelErr(dataSet): ws,X,Y=linearSolve(dataSet) yHat=X*ws return sum(power(Y-yHat,2))
哇,可是线性回归又是怎么回事呢,别急,代码中给到的,一会儿会整理。
补充:线性回归(当然会单列专题来说这个事情)
利用线性模型可以尝试去搞一个线性组合来进行预测,函数形式如下:
为了让线性模型尽可能的完善,我们需要尽可能的减少均方误差:
在线性回归中,最小二乘法就是尝试去找一条直线,让所有的样本到这个直线上的欧式距离之和最小,也就是上述公式,为了让其得到最小值,我们通常需要对式子对未知数w向量和求导并设置为0可以进行求解。
为了方便讨论我们通常将w向量和b放在一起进行讨论,设定一个全新的矩阵D,D的前n-1列恒定为属性值,最后一列设定初始值为1,实际上就是wx+b的形式,因为b的参数是1嘛,因此得到的误差函数为:
求导并另其为0,得到线性回归公式:
这个也就是上面公式的由来,啧啧