CART(classificatiion and regression trees)分类回归树
- CART既能用于分类,也能用于回归
- CART是二叉树
CART算法由决策树的生成和决策树的剪枝两步组成。
1 CART生成
1.1回归树
回归树采用平方误差最小化准则,分类树采用基尼指数最小化准则,进行特征选择,生成二叉树。
1.2 分类树
分类树采用基尼指数选择最优特征,同时决定该值的最优二值切分点。
2 CART剪枝
首先从生成算法产生的决策树T0底端开始不断剪枝,直到T0的根节点,形成一个子树序列{T0,T1,…,Tn};然后通过交叉验证法在独立的验证数据集上对子树序列进行测试,从中选择最优子树
1.剪枝,形成子树序列
剪枝过程中,计算子树的损失函数:
其中T为任意子树,|T|为树T的节点个数,参数a权衡训练数据的拟合程度与模型的复杂度。
可以用递归的方法对树进行剪枝,将a从小增大,a0<a1<...<an<+无穷,产生一系列的区间[ai,ai+1),i =0,1,...,n;剪枝得到的子树序列对应着区间[ai,ai+1),i =0,1,...,n的最优子树序列{T0, T1, ... , Tn},序列中的子树是嵌套的。
对T0中每一内部结点t,计算
3 CART算法实现
3.1 CART算法在回归中的应用
3.1.1构建树
算法:
对每个特征:
对特征的每个取值:
将数据集切分成两部分
计算切分的误差
如果当前误差小于当前最小误差,则更新当前最小误差,并更新最佳切分属性和切分值
返回最佳切分的特征和切分值
import numpy as np
from matplotlib import pyplot as plt
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine) #将数据映射成浮点型,返回的是map的地址
dataMat.append(list(fltLine))
return dataMat
def plotData(dataMat):
x = dataMat[:,0].tolist()
y = dataMat[:,1].tolist()
plt.scatter(x,y)
plt.title('DataSet')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
def plotData2(dataMat):
x = dataMat[:, 1].tolist()
y = dataMat[:, 2].tolist()
plt.scatter(x, y)
plt.title('DataSet')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据某个特征及值对数据集进行切分
:param dataSet: 数据集
:param feature: 特征
:param value: 特征值
:return: 切分后的数据
"""
mat0 = dataSet[np.nonzero(dataSet[:,feature] > value),:][0]
mat1 = dataSet[np.nonzero(dataSet[:,feature] <= value),:][0]
return mat0, mat1
"""
下面构建回归树
"""
def regLeaf(dataSet):
"""
函数说明:生成叶节点
:param dataSet: 数据集
:return: 使用均值作为叶节点的值
"""
# print(dataSet)
return np.mean(dataSet[:,-1])
def regErr(dataSet):
"""
函数说明:误差估计函数
:param dataSet: 数据集
:return: 总方差
"""
return np.var(dataSet[:-1]) * np.shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
"""
函数说明:找到数据的最佳二元切分方式函数
:param dataSet: 数据将
:param leafType: 生成叶节点
:param errType: 误差估计函数
:param ops:用户自定义参数构成的元组
:return:
bestIndex :最佳切分特征的下标
bestValue: 最优切分值
"""
"""
伪代码:
首先判断是否所有值相等:
否:计算误差值,初始化最佳误差,最优切分特征下标,最优切分值
遍历每个特征:
遍历特征的每个值:
将数据切分为两部分
判断是否小于最少切分样本值:
是:跳出循环
否:计算切分后误差
如果切分后误差小于当前最佳误差,则更新最优切分特征下标,最优切分值,最优误差
如果误差下降值小于最小误差值,则返回最佳切分特征值和最优切分值
"""
tolS = ops[0]; tolN = ops[1] #tolS是允许的最小误差下降值,tolN是切分的最少样本
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果所有值相等则退出,根据set特性
return None, leafType(dataSet)
m,n = np.shape(dataSet)
#the choice of the best feature is driven by Reduction in RSS error from mean
S = errType(dataSet) #计算误差值
bestS = np.inf; bestIndex = 0; bestValue = 0
for featIndex in range(n-1): #遍历每个特征值
for splitVal in set((dataSet[:,featIndex].T.tolist())[0]): #遍历特征的每个值
# print(featIndex, splitVal)
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal) #将数据集切分为两部分
if (np.shape(mat0)[0] < tolN) or (np.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) #exit cond 2
#切分数据集
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
#如果切分后的数据集个数小于tolN,则不进行切分
# if (np.shape(mat0)[0] < tolN) or (np.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: 树
"""
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
if __name__ == '__main__':
ex00Data = loadDataSet('txt/ex00.txt')
ex00Mat = np.mat(ex00Data)
plotData(ex00Mat)
myTree1 = createTree(ex00Mat)
print(myTree1)
ex0Data = loadDataSet('txt/ex0.txt')
ex0Mat = np.mat(ex0Data)
plotData2(ex0Mat)
myTree2 = createTree(ex0Mat)
print(myTree2)
print(createTree(ex0Mat, ops=(0,1))) #tolS为0,tolN为1
生成结果如下:
3.1.2 树剪枝
为了防止过拟合,需要对树进行剪枝
(1)预剪枝
if __name__ == '__main__':
ex2Data = loadDataSet('txt/ex2.txt')
ex2Mat = np.mat(ex2Data)
plotData(ex2Mat)
ex2Tree = createTree(ex2Mat)
print(ex2Tree) #非常多的叶子节点
print(createTree(ex2Mat,ops=(10000,4))) #只有两个叶子节点,tolS对误差的数据集非常敏感
在生成树的过程中进行剪枝,如前面设置的最低下降误差值tolS和最小节点数tolN
使用ex2.txt来构建树,ex2.txt的数量级是ex0.txt的100倍
可以看出停止条件tolS对误差的数量级十分敏感。
(2)后剪枝
使用后剪枝需要将数据集分成测试集和训练集。
剪枝函数的伪代码:
代码如下:
import numpy as np
from matplotlib import pyplot as plt
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine) #将数据映射成浮点型,返回的是map的地址
dataMat.append(list(fltLine))
return dataMat
def plotData(dataMat):
x = dataMat[:,0].tolist()
y = dataMat[:,1].tolist()
plt.scatter(x,y)
plt.title('DataSet')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
def plotData2(dataMat):
x = dataMat[:, 1].tolist()
y = dataMat[:, 2].tolist()
plt.scatter(x, y)
plt.title('DataSet')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据某个特征及值对数据集进行切分
:param dataSet: 数据集
:param feature: 特征
:param value: 特征值
:return: 切分后的数据
"""
mat0 = dataSet[np.nonzero(dataSet[:,feature] > value),:][0]
mat1 = dataSet[np.nonzero(dataSet[:,feature] <= value),:][0]
return mat0, mat1
"""
下面构建回归树
"""
def regLeaf(dataSet):
"""
函数说明:生成叶节点
:param dataSet: 数据集
:return: 使用均值作为叶节点的值
"""
# print(dataSet)
return np.mean(dataSet[:,-1])
def regErr(dataSet):
"""
函数说明:误差估计函数
:param dataSet: 数据集
:return: 总方差
"""
return np.var(dataSet[:-1]) * np.shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
"""
函数说明:找到数据的最佳二元切分方式函数
:param dataSet: 数据将
:param leafType: 生成叶节点
:param errType: 误差估计函数
:param ops:用户自定义参数构成的元组
:return:
bestIndex :最佳切分特征的下标
bestValue: 最优切分值
"""
"""
伪代码:
首先判断是否所有值相等:
否:计算误差值,初始化最佳误差,最优切分特征下标,最优切分值
遍历每个特征:
遍历特征的每个值:
将数据切分为两部分
判断是否小于最少切分样本值:
是:跳出循环
否:计算切分后误差
如果切分后误差小于当前最佳误差,则更新最优切分特征下标,最优切分值,最优误差
如果误差下降值小于最小误差值,则返回最佳切分特征值和最优切分值
"""
tolS = ops[0]; tolN = ops[1] #tolS是允许的最小误差下降值,tolN是切分的最少样本
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果所有值相等则退出,根据set特性
return None, leafType(dataSet)
m,n = np.shape(dataSet)
#the choice of the best feature is driven by Reduction in RSS error from mean
S = errType(dataSet) #计算误差值
bestS = np.inf; bestIndex = 0; bestValue = 0
for featIndex in range(n-1): #遍历每个特征值
for splitVal in set((dataSet[:,featIndex].T.tolist())[0]): #遍历特征的每个值
# print(featIndex, splitVal)
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal) #将数据集切分为两部分
if (np.shape(mat0)[0] < tolN) or (np.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) #exit cond 2
#切分数据集
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
#如果切分后的数据集个数小于tolN,则不进行切分
# if (np.shape(mat0)[0] < tolN) or (np.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: 树
"""
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
def isTree(obj):
"""
函数说明:判断是否是一棵树
:param obj:
:return:
"""
return (type(obj).__name__=='dict')
def getMean(tree):
"""
函数说明:递归函数,对树进行塌陷处理(返回树平均值)
:param tree: 树
:return: 树的左右子树的均值
"""
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 prnue(tree, testData):
"""
函数说明:后剪枝函数,使用测试数据对树进行剪枝
:param tree: 待剪枝的树
:param testData: 测试数据
:return: 剪枝后的树
"""
"""
树的后剪枝:
伪代码:
基于已有的树切分测试数据:
如果存在任一子集是一棵树,则在该子集递归剪枝过程
计算将当前两个叶节点合并后的误差
计算没有合并时的误差
如果合并可以降低误差则进行合并
"""
#如果测试数据集为空,进行塌陷处理
if(np.shape(testData[0]) == 0): return getMean(tree)
# if isTree(tree['left']) and isTree(tree['right']):
lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
if isTree(tree['left']): tree['left'] = prnue(tree['left'], lSet) #左子集不是单个节点,则对左子树进行剪枝
if isTree(tree['right']): tree['right'] = prnue(tree['right'], rSet) #右子集不是单个节点,则对右子树进行剪枝
if not isTree(tree['left']) and not isTree(tree['right']): #左右子集都是单个节点
errNoMerge = sum(np.power((lSet[:,-1] - tree['left']),2)) + sum(np.power((rSet[:,-1] - tree['right']),2)) #未合并的误差
treeMean = getMean(tree)
errMerge = sum(np.power(testData[:,-1] - treeMean, 2)) #合并后误差
print(errNoMerge, errMerge)
if errMerge < errNoMerge: #如果合并后误差小于合并前误差则合并
print("merging")
return treeMean
else: return tree
else: return tree
if __name__ == '__main__':
ex2Data = loadDataSet('txt/ex2.txt')
ex2Mat = np.mat(ex2Data)
plotData(ex2Mat)
ex2Tree = createTree(ex2Mat)
print(ex2Tree)
ex2TestData = loadDataSet('txt/ex2test.txt')
prnueTree = prnue(ex2Tree, np.mat(ex2TestData)) #对ex2Tree进行剪枝
print(prnueTree)
3.2 模型树
模型树的节点是线性回归函数
模型树的代码:
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine) #将数据映射成浮点型,返回的是map的地址
dataMat.append(list(fltLine))
return dataMat
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据某个特征及值对数据集进行切分
:param dataSet: 数据集
:param feature: 特征
:param value: 特征值
:return: 切分后的数据
"""
mat0 = dataSet[np.nonzero(dataSet[:,feature] > value),:][0]
mat1 = dataSet[np.nonzero(dataSet[:,feature] <= value),:][0]
return mat0, mat1
def linearSolve(dataSet):
"""
函数说明:生成数据集的线性模型
:param dataSet: 数据集
:return: 系数,X,Y
"""
m, n = np.shape(dataSet)
X = np.mat(np.ones((m, n))); Y = np.mat(np.zeros((m, 1)))
X[:,1:n] = dataSet[:,0:n-1]
Y = dataSet[:,-1]
xTx = X.T * X
# linalg.det()计算行列式,若为0,则不可逆
if np.linalg.det(xTx) == 0.0:
print("This matrix is singular, cannot do inverse")
return
# 回归系数ws = (X^TX)^-1X^Ty
ws = xTx.I * (X.T * Y)
return ws, X, Y
def modelLeaf(dataSet):
"""
函数说明:线性模型树的叶节点生成函数
:param dataSet:
:return:
"""
ws, X, Y = linearSolve(dataSet)
return ws
def modelErr(dataSet):
"""
函数说明:线性模型误差计算函数
:param dataSet: 数据集
:return: 误差
"""
ws, X, Y = linearSolve(dataSet)
yHat = X * ws
return sum(np.power((Y - yHat), 2))
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
"""
函数说明:找到数据的最佳二元切分方式函数
:param dataSet: 数据将
:param leafType: 生成叶节点
:param errType: 误差估计函数
:param ops:用户自定义参数构成的元组
:return:
bestIndex :最佳切分特征的下标
bestValue: 最优切分值
"""
"""
伪代码:
首先判断是否所有值相等:
否:计算误差值,初始化最佳误差,最优切分特征下标,最优切分值
遍历每个特征:
遍历特征的每个值:
将数据切分为两部分
判断是否小于最少切分样本值:
是:跳出循环
否:计算切分后误差
如果切分后误差小于当前最佳误差,则更新最优切分特征下标,最优切分值,最优误差
如果误差下降值小于最小误差值,则返回最佳切分特征值和最优切分值
"""
tolS = ops[0]; tolN = ops[1] #tolS是允许的最小误差下降值,tolN是切分的最少样本
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果所有值相等则退出,根据set特性
return None, leafType(dataSet)
m,n = np.shape(dataSet)
#the choice of the best feature is driven by Reduction in RSS error from mean
S = errType(dataSet) #计算误差值
bestS = np.inf; bestIndex = 0; bestValue = 0
for featIndex in range(n-1): #遍历每个特征值
for splitVal in set((dataSet[:,featIndex].T.tolist())[0]): #遍历特征的每个值
# print(featIndex, splitVal)
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal) #将数据集切分为两部分
if (np.shape(mat0)[0] < tolN) or (np.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) #exit cond 2
#切分数据集
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
#如果切分后的数据集个数小于tolN,则不进行切分
# if (np.shape(mat0)[0] < tolN) or (np.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: 树
"""
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
if __name__ == '__main__':
exp2Data = loadDataSet('txt/exp2.txt')
exp2Mat = np.mat(exp2Data)
exp2Tree = createTree(exp2Mat, modelLeaf, modelErr, (1,10)) #模型树
print(exp2Tree)
3.3 进行预测——简单线性回归 回归树 模型树的对比
使用决定系数R^2值来判断预测效果,越接近于1越好
#!/usr/bin/env python
# encoding: utf-8
'''
@author: shuhan Wei
@software: pycharm
@file: treeFore.py
@time: 18-9-13 下午6:33
@desc:树回归预测
'''
import regTrees
import numpy as np
def regTreeEval(model, inDat):
"""
函数说明:回归树的单个节点预测值
:param model: 某一节点的值
:param inDat: 空值
:return: 预测的浮点数
"""
return float(model)
def modelTreeEval(model, inDat):
"""
函数说明:模型树的预测值
:param model: 预测节点的回归系数
:param inDat: 某个测试数据
:return: 预测值
"""
n = np.shape(inDat)[1]
X = np.mat(np.ones((1, n + 1)))
X[:, 1:n + 1] = inDat
return float(X * model)
def treeForeCast(tree, inData, modelEval=regTreeEval):
if not regTrees.isTree(tree): return modelEval(tree, inData)
if inData[tree['spInd'],0] > tree['spVal']:
if regTrees.isTree(tree['left']):
return treeForeCast(tree['left'], inData, modelEval)
else:
return modelEval(tree['left'], inData)
else:
if regTrees.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 = np.mat(np.zeros((m, 1)))
for i in range(m):
yHat[i, 0] = treeForeCast(tree, np.mat(testData[i]), modelEval)
return yHat
if __name__ == '__main__':
trainMat = np.mat(regTrees.loadDataSet('txt/bikeSpeedVsIq_train.txt'))
testMat = np.mat(regTrees.loadDataSet('txt/bikeSpeedVsIq_test.txt'))
regTrees.plotData(trainMat)
# 使用回归树模型进行预测
regTree = regTrees.createTree(trainMat, ops=(1,20))
yHat = createForeCast(regTree, testMat, regTreeEval)
#计算R^2值,越接近于1越好
cor1 = np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0,1]
print(cor1)
#使用模型树进行预测
modelTree = regTrees.createTree(trainMat, regTrees.modelLeaf, regTrees.modelErr, (1,20))
yHat = createForeCast(modelTree, testMat[:, 0], modelTreeEval)
col2 = np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0, 1]
print(col2)
#使用简单线性模型
ws, X, Y = regTrees.linearSolve(trainMat)
for i in range(np.shape(testMat)[0]):
yHat[i] = testMat[i, 0] * ws[1, 0] + ws[0, 0]
col3 = np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0, 1]
print(col3)
可以看到数据集的分布像两段线性函数,从R^2值可以看出模型树的预测效果最好。