上篇博文介绍了ID3决策树的基本原理,ID3的缺陷是没有剪枝操作(容易过拟合),无法处理连续数据,而且直接使用信息增益作为判别分裂属性的选择也有问题,详情请看相关文章介绍C45决策树的,这里我就不去多介绍了,我的目的是跟着《Machine in Action》这本书进行学习,讲解最基础的算法。
回归也是有监督学习算法的一种,可以理解为连续取值标签的分类。传统的分类算法是依据属于某个类别的概率(得分等)判断属于某一个类,那决策树如何实现回归哪?上一篇博文中,我介绍了如何根据属性进行分裂数据集,不断的进行上述操作,生成一个树。被划分在同一个数据集中的数据(同一个子树)显然有某种相似的特征,CRAT决策树就是利用这种方法实现回归算法的。比如平面上有如下几个点:
import matplotlib.pyplot as plt
plt.figure(1)
plt.scatter([0.8,1,1.2,2.8,3.0,3.2],[0.9,1.0,1.1,2.9,3.0,3.1])
plt.show()
我现在有x=0.9这个点,它的取值是多少(y)?如果使用决策树,会这样操作:0.9会被划分到(0.8,0.9),(1,1),(1.2,1.1)这三个点构成的子叶点上,那么我就可以使用这三个点的均值估计x=0.9时y的取值y=1。具体的算法不仅要选择分裂属性,而且还要选择分裂属性的分裂值,具体来讲其实CRAT就是一个二叉树,每一次选择一个属性进行二次划分,直到满足停止条件为止。那么停止条件是什么哪?停止条件可以是:树的深度,每一个属性最多被划分的次数,每一个树最少的数据数目,数据集的目标值相等,或者误差很小等等。每一个属性分裂值的选择是根据误差最小的方法来判断,即判断分裂的左右数据集的误差,这个误差定义为方差(因为最后判断的准则是赋予均值,所以偏离均值的大小可以作为衡量的标准)。CRAT回归树还有一个重要的性质,就是进行剪枝操作,能够预防过拟合(关于过拟合如果都不知道的话,呵呵。。。)。如何进行剪枝操作,有很多种,比如前面介绍的停止条件: 树的深度,每一个属性最多被划分的次数,每一个树最少的数据数目等称为前剪枝,而应用最多的是后剪枝:如果测试集没有到达某一个节点(或者数目比较少),则可以撤销这个节点变为叶子节点(用左右叶子节点数值的均值作为新叶子节点的值);若果把该节点变为叶子节点时误差也会随之减小,则进行剪枝操作(过程和上一步骤一样),具体过程还是直接看代码吧(代码来自于Machine Learning Action):
''' Created on Feb 4, 2011 Tree-Based Regression Methods @author: Peter Harrington '''
from numpy import *
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float,curLine) #使用map函数将数据转换为float
dataMat.append(fltLine)
return dataMat
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 regLeaf(dataSet):#返回叶子节点的平均值
return mean(dataSet[:,-1])
def regErr(dataSet):#这里误差的定义为偏离均值的平方和,方差的平方和
return var(dataSet[:,-1]) * shape(dataSet)[0]
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('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,X,Y = linearSolve(dataSet)
return ws
def modelErr(dataSet):#计算误差函数,这里的误差函数经过了高斯函数的转换
ws,X,Y = linearSolve(dataSet)
yHat = X * ws
return sum(power(Y - yHat,2))
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: #exit cond 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#如果分裂后某一个子数据集的数量小于tolN,则跳入下一个循环
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#返回分裂属性的index和value
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)#choose the best split
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
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#继续进行剪枝
def regTreeEval(model, inDat):
return float(model)
def modelTreeEval(model, inDat):
n = shape(inDat)[1]
X = mat(ones((1,n+1)))
X[:,1:n+1]=inDat
return float(X*model)
#预测
def treeForeCast(tree, inData, modelEval=regTreeEval):
if not isTree(tree): return modelEval(tree, inData)
if inData[tree['spInd']] > tree['spVal']:
if isTree(tree['left']): return treeForeCast(tree['left'], inData, modelEval)
else: return modelEval(tree['left'], inData)
else:
if isTree(tree['right']): return treeForeCast(tree['right'], inData, modelEval)
else: return modelEval(tree['right'], inData)
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
main.py
main.py
from regTrees import *
myDat = loadDataSet("ex00.txt")
myMat = mat(myDat)
print createTree(myMat)