树回归
分类回归树(Classification And Regression Trees,CART)是一种构造树的监督学习方法。
和ID3决策树作比较:
1. ID3每次直接用最佳特征分割数据,即如果当前特征有4个可能值,那么数据将被分成4份,处理的是标称型数据,不能直接处理连续型数据。CART则利用二元切分来处理连续型变量,每次会找一个最佳特征的阈值,把数据集分成两部分,也就是左子树和右子树。
2. CART使用方差计算来代替香农熵。但目的都是找最佳切分特征
树回归将数据集切分成多份易建模的数据,然后利用线性回归进行建模和拟合。
寻找最佳特征算法伪代码:
如果该数据集的特征值只有一种,不可切分,返回当前结点的数据均值作为特征值
否则重复一下步骤直到找到最小总方差
遍历每一列
遍历每列的值
用该值切分数据
计算总方差
如果总方差差值小于最初设定的阈值,不可切分
如果左右样本数小于最初设定的阈值,不可切分
否则返回最佳特征和最佳特征值。
构建回归树算法伪代码:
寻找当前最佳待切特征和特征值并返回
如果当前最佳特征没有找到,不可切分,则把当前结点的数据均值作为叶节点
否则用最佳特征和特征值构建当前结点
切分后的左右节点分别递归以上算法
需要输入的参数有:数据集,叶节点模型函数(均值),误差估计函数(总方差),允许的总方差最小下降值,节点最小样本数。
1.2、树构建算法 比较
我们在 第3章 中使用的树构建算法是 ID3 。ID3 的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有 4 种取值,那么数据将被切分成 4 份。一旦按照某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。另外一种方法是二元切分法,即每次把数据集切分成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入树的右子树。
除了切分过于迅速外, ID3 算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征转换成离散型,才能在 ID3 算法中使用。但这种转换过程会破坏连续型变量的内在性质。而使用二元切分法则易于对树构造过程进行调整以处理连续型特征。具体的处理方法是: 如果特征值大于给定值就走左子树,否则就走右子树。另外,二元切分法也节省了树的构建时间,但这点意义也不是特别大,因为这些树构建一般是离线完成,时间并非需要重点关注的因素。
CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量。对 CART 稍作修改就可以处理回归问题。第 3 章中使用香农熵来度量集合的无组织程度。如果选用其他方法来代替香农熵,就可以使用树构建算法来完成回归。
回归树与分类树的思路类似,但是叶节点的数据类型不是离散型,而是连续型。
实现代码
#!/usr/bin/env python
# encoding: utf-8
from numpy import *
def loadDataSet(filename):
'''
curLine : 当前分割后的字符串数组
fltLine : 被映射成浮点数型的数组
:param filename: 文件路径
:return:
dataMat : 数据集
'''
dataMat = []
fr = open(filename)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float, curLine)
print("curLine :" + curLine, end= "\n")
print("fltLine :" + fltLine, end= "\b")
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):
'''
:param dataSet:
:param feature: 特征向量的列数
:param value: 比较的值
:return:
mat0: 矩阵中特征向量列大于value值的元素所组成的矩阵
mat1: 矩阵中特征向量列大于value值的元素所组成的矩阵
'''
# nonzero函数返回array中非零元素的索引 二维表示为(行索引, 列索引)
mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0], :]
# print('dataSet[:, feature]', dataSet[:, feature])
# print('nonzero(dataSet[:, feature] > value)', nonzero(dataSet[:, feature] > value))
# print('dataSet[nonzero(dataSet[:, feature] > value)[0], :]', dataSet[nonzero(dataSet[:, feature] > value)[0], :])
mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :]
# print('dataSet[:, feature]', dataSet[:, feature])
# print('nonzero(dataSet[:, feature] > value)', nonzero(dataSet[:, feature] > value))
# print('dataSet[nonzero(dataSet[:, feature] > value)[0], :]', dataSet[nonzero(dataSet[:, feature] > value)[0], :])
return mat0, mat1
def regLeaf(dataSet):
'''
用于生成叶节点, 理解是通过求均值,来用中心点代表这类数据
:param dataSet:
:return:
'''
#计算指定轴上的算术平均值 默认为将向量组展开成一行所得的算数平均值
#`mean(a, axis=None, dtype=None, out=None, keepdims=np._NoValue)`
#a包含期望平均值的数组 不是数组需转化 axis 指定轴 dtype平均值类型 keepdims如果将其设置为True,则缩小的轴将保留为尺寸1的尺寸。使用此选项,结果将针对输入数组正确广播。
return mean(dataSet[:, 1])
def regErr(dataSet):
'''
用于估计误差 计算目标变量的平方误差 通过决策树划分, 可以让靠近的数据分到同一类中
:param dataSet:
:return:
平方差之和, 即总方差
'''
#计算指定轴上的方差 默认为将向量组展开成一行所得的方差
return var(dataSet[:, 1]) * shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops = (1,4)):
'''
找出数据最佳的二分切分方式, 如果找不到一个好的二元分割,会返回None,并产生叶节点
tolS 为允许的误差下降值
tolN 为最小的切割样本数
:param dataSet:
:param leafType:
:param errType:
:param ops:
:return:
bestIndex feature的index坐标
bestValue 切分的最优值
'''
tolS = ops[0]
tolN = ops[1]
if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
return None, leafType(dataSet)
m, n = shape(dataSet)
# 初始化数据
S = errType(dataSet)
bestS = inf
bestIndex = 0
bestValue = 0
#featIndex 某一列
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
# 计算新方差和
news = errType(mat0) + errType(mat1)
# 如果新方差小 则更新数据
if news < bestS:
bestIndex = featIndex
bestValue = splitVal
bestS = news
if (S - bestS) < tolS:
return None, leafType(dataSet)
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
if(shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
return None, leafType(dataSet)
return bestIndex, bestValue
def createTree(dataSet, leafType = regLeaf, errType = regErr, ops = (1, 4)):
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:
布尔类型结果。如果obj是一个字典返回true
'''
return (type(obj).__name__ == 'dict')
def getMean(tree):
'''
遍历树的结点 直到叶节点为止,如果找到两个叶节点就计算它们的平均值
:param tree:
:return:
'''
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['rightt'], 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 modelLeaf(dataSet):
"""
Desc:
当数据不再需要切分的时候,生成叶节点的模型。
Args:
dataSet -- 输入数据集
Returns:
调用 linearSolve 函数,返回得到的 回归系数ws
"""
ws, X, Y = linearSolve(dataSet)
return ws
# 计算线性模型的误差值
def modelErr(dataSet):
"""
Desc:
在给定数据集上计算误差。
Args:
dataSet -- 输入数据集
Returns:
调用 linearSolve 函数,返回 yHat 和 Y 之间的平方误差。
"""
ws, X, Y = linearSolve(dataSet)
yHat = X * ws
# print corrcoef(yHat, Y, rowvar=0)
return sum(power(Y - yHat, 2))
# helper function used in two places
def linearSolve(dataSet):
"""
Desc:
将数据集格式化成目标变量Y和自变量X,执行简单的线性回归,得到ws
Args:
dataSet -- 输入数据
Returns:
ws -- 执行线性回归的回归系数
X -- 格式化自变量X
Y -- 格式化目标变量Y
"""
m, n = shape(dataSet)
# 产生一个关于1的矩阵
X = mat(ones((m, n)))
Y = mat(ones((m, 1)))
# X的0列为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,\ntry increasing the second value of ops')
# 最小二乘法求最优解: w0*1+w1*x1=y
ws = xTx.I * (X.T * Y)
return ws, X, Y
# 回归树测试案例
# 为了和 modelTreeEval() 保持一致,保留两个输入参数
def regTreeEval(model, inDat):
"""
Desc:
对 回归树 进行预测
Args:
model -- 指定模型,可选值为 回归树模型 或者 模型树模型,这里为回归树
inDat -- 输入的测试数据
Returns:
float(model) -- 将输入的模型数据转换为 浮点数 返回
"""
return float(model)
# 模型树测试案例
# 对输入数据进行格式化处理,在原数据矩阵上增加第0列,元素的值都是1,
# 也就是增加偏移值,和我们之前的简单线性回归是一个套路,增加一个偏移量
def modelTreeEval(model, inDat):
"""
Desc:
对 模型树 进行预测
Args:
model -- 输入模型,可选值为 回归树模型 或者 模型树模型,这里为模型树模型,实则为 回归系数
inDat -- 输入的测试数据
Returns:
float(X * model) -- 将测试数据乘以 回归系数 得到一个预测值 ,转化为 浮点数 返回
"""
n = shape(inDat)[1]
X = mat(ones((1, n+1)))
X[:, 1: n+1] = inDat
# print X, model
return float(X * model)
# 计算预测的结果
# 在给定树结构的情况下,对于单个数据点,该函数会给出一个预测值。
# modelEval是对叶节点进行预测的函数引用,指定树的类型,以便在叶节点上调用合适的模型。
# 此函数自顶向下遍历整棵树,直到命中叶节点为止,一旦到达叶节点,它就会在输入数据上
# 调用modelEval()函数,该函数的默认值为regTreeEval()
def treeForeCast(tree, inData, modelEval=regTreeEval):
"""
Desc:
对特定模型的树进行预测,可以是 回归树 也可以是 模型树
Args:
tree -- 已经训练好的树的模型
inData -- 输入的测试数据,只有一行
modelEval -- 预测的树的模型类型,可选值为 regTreeEval(回归树) 或 modelTreeEval(模型树),默认为回归树
Returns:
返回预测值
"""
if not isTree(tree):
return modelEval(tree, inData)
# 书中写的是inData[tree['spInd']],只适合inData只有一列的情况,否则会产生异常
if inData[0, tree['spInd']] <= tree['spVal']:
# 可以把if-else去掉,只留if里面的分支
if isTree(tree['left']):
return treeForeCast(tree['left'], inData, modelEval)
else:
return modelEval(tree['left'], inData)
else:
# 同上,可以把if-else去掉,只留if里面的分支
if isTree(tree['right']):
return treeForeCast(tree['right'], inData, modelEval)
else:
return modelEval(tree['right'], inData)
# 预测结果
def createForeCast(tree, testData, modelEval=regTreeEval):
"""
Desc:
调用 treeForeCast ,对特定模型的树进行预测,可以是 回归树 也可以是 模型树
Args:
tree -- 已经训练好的树的模型
testData -- 输入的测试数据
modelEval -- 预测的树的模型类型,可选值为 regTreeEval(回归树) 或 modelTreeEval(模型树),默认为回归树
Returns:
返回预测值矩阵
"""
m = len(testData)
yHat = mat(zeros((m, 1)))
# print yHat
for i in range(m):
yHat[i, 0] = treeForeCast(tree, mat(testData[i]), modelEval)
# print "yHat==>", yHat[i, 0]
return yHat
if __name__ == "__main__":
# 测试数据集
testMat = mat(eye(4))
print(testMat)
print(type(testMat))
mat0, mat1 = binSplitDataSet(testMat, 1, 0.5)
print(mat0, '\n-----------\n', mat1)
# # 回归树
# myDat = loadDataSet('data/9.RegTrees/data1.txt')
# # myDat = loadDataSet('data/9.RegTrees/data2.txt')
# # print 'myDat=', myDat
# myMat = mat(myDat)
# # print 'myMat=', myMat
# myTree = createTree(myMat)
# print myTree
# # 1. 预剪枝就是:提起设置最大误差数和最少元素数
# myDat = loadDataSet('data/9.RegTrees/data3.txt')
# myMat = mat(myDat)
# myTree = createTree(myMat, ops=(0, 1))
# print myTree
# # 2. 后剪枝就是:通过测试数据,对预测模型进行合并判断
# myDatTest = loadDataSet('data/9.RegTrees/data3test.txt')
# myMat2Test = mat(myDatTest)
# myFinalTree = prune(myTree, myMat2Test)
# print '\n\n\n-------------------'
# print myFinalTree
# # --------
# # 模型树求解
# myDat = loadDataSet('data/9.RegTrees/data4.txt')
# myMat = mat(myDat)
# myTree = createTree(myMat, modelLeaf, modelErr)
# print myTree
# # # 回归树 VS 模型树 VS 线性回归
# trainMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_train.txt'))
# testMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_test.txt'))
# # # 回归树
# myTree1 = createTree(trainMat, ops=(1, 20))
# print myTree1
# yHat1 = createForeCast(myTree1, testMat[:, 0])
# print "--------------\n"
# # print yHat1
# # print "ssss==>", testMat[:, 1]
# # corrcoef 返回皮尔森乘积矩相关系数
# print "regTree:", corrcoef(yHat1, testMat[:, 1],rowvar=0)[0, 1]
# # 模型树
# myTree2 = createTree(trainMat, modelLeaf, modelErr, ops=(1, 20))
# yHat2 = createForeCast(myTree2, testMat[:, 0], modelTreeEval)
# print myTree2
# print "modelTree:", corrcoef(yHat2, testMat[:, 1],rowvar=0)[0, 1]
# # 线性回归
# ws, X, Y = linearSolve(trainMat)
# print ws
# m = len(testMat[:, 0])
# yHat3 = mat(zeros((m, 1)))
# for i in range(shape(testMat)[0]):
# yHat3[i] = testMat[i, 0]*ws[1, 0] + ws[0, 0]
# print "lr:", corrcoef(yHat3, testMat[:, 1],rowvar=0)[0, 1]