树回归
树回归的有点事可以对复杂和非线性的数据建模,缺点则是结果不易理解,使用于数值型和标称型数据。
一般来说树的构建算法是ID3,每次选取当前最佳的特征来分割数据,并按该特征的所有可能的取值来切分。如果一个特征有4种取值,那么数据将被截成4份。另一种是二元切分法,每次把数据集切成两份,如果数据的某特征值等于切分所要求的值,那么这些数据就进入左子树,反之进入右子树。ID3算法还不能直接处理连续型特征,需要转换成离散型。但这种准换过程会破坏连续性变量的内在性质。二元切分法易于对书构建过程进行调整已处理连续型特征。
下面实观CART算法和回归树。回归树叶节点的数据类型不是离散型而是连续性。
使用一部分字典来存储树的数据结构,包含以下四个元素:
1.待切分的特征
2、待切分的特征值
3.右子树,当不需要切分时,也可以是个单值
4.左子树,与右子树类似
首先建立树节点:
class treeNode():
def __init__(self,feat,val,right,left):
featureToSplitOn = feat
valueOfSplit = val
rightBranch = right
leftBranch =left
"""
用来建立树节点
"""
这里会构建两种树,第一种是回归树,期每个叶节点包含单个值。第二种是模型树,期每个叶节点包含一个线性方程。
先给出一些共用代码,函数createTree()的伪代码如下:
找到最佳的待切分特征:
如果该节点不能再分,将该节点存为叶节点
执行二元切分
在右子树调用createTree()方法
在左子树调用createTree()方法
from numpy import *
def loadDataSet(filename):
dataMat = []
fr = open(filename)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float,curLine)
dataMat.append(fltLine)
return dataMat
def regLeaf(dataSet):
return mean(dataSet[:,-1])
#负责生成叶节点。当chooseBestSplit()函数确定不再对数据进行切分时,将调用该函数
#来得到叶节点的模型,该模型其实就是目标变量的均值
def regErr(dataSet):
return var(dataSet[:,-1]) * shape(dataSet)[0]
#该函数在给定数据上计算目标变量的平方误差。
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
tolS = ops[0]; tolN = ops[1]
#用于控制函数的停止时机,其中tols是容许的误差下降值,tolN是切分的最少样本数
#如果所有目标变量的值相同:退出并返回值
if len(set(dataSet[:,-1].T.tolist()[0])) == 1:
return None, leafType(dataSet)
#如果找不到好的二元切分,该函数将返回None斌同时调用creeatTree()方法来产生叶节点
m,n = shape(dataSet)
#最佳特性的选择是通过减少平均值的RSS误差来实现的
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
newS = errType(mat0) + errType(mat1)
if newS < bestS:
bestIndex = featIndex
bestValue = splitVal
bestS = newS
#如果减少(S-BESTS)小于阈值,则不执行分割。
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 binSplitDataSet(dataSet,feature,value):
mat0 = dataSet[nonzero(dataSet[:,feature]>value)[0],:][0]
mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :][0]
return mat0,mat1
"""
该函数有三个参数,数据集合,待切分的特征和该特征的某个值
在给定特征和特征值的情况下,该函数通过数组过滤方式将上述数据集合切分的到两个子集并返回
"""
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
"""
这个函数有四个参数:数据集和其他三个可选参数,这些可选参数决定了树的类型
leafType给出建立叶节点的函数
errType代表误差计算函数
ops是一个包含树构建所需要其他参数的元组
该函数是一个递归函数,首先尝试将数据集分成两部分,切分由函数chooseBestSplit()来完成
如果满足停止条件,chooseBestSplit()将返回None和某模型的值。
如果构建的是回归树,该模型是一个常数。如果是模型树,其模型是一个线性方程。
如果不满足停止条件,chooseBestSplit()将创建一个新的字典并将数据集分成两份,在这两份数据集上
分别继续递归调用createTree()函数
"""
testMat = mat(eye(4))
print(testMat)
print("-----------------------------------")
mat0,mat1 = binSplitDataSet(testMat,1,0.5)
print(mat0)
print("-----------------------------------")
print(mat1)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
-----------------------------------
[[0. 1. 0. 0.]]
-----------------------------------
[[1. 0. 0. 0.]]
testMat创建了一个简单的矩阵,下面是按照指定列的某个值来切分该矩阵。
chooseBestSplit()函数的伪代码如下:该函数的目标是找到数据集切分的最佳位置,他遍历所有的特征及其可能的取值来找到谁误差最小化的切分阈值。
对每个特征值:
对每个特征值:
将数据集切分成两份
计算切分的误差
如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
返回最佳切分的特征和阈值
看一下代码的实际效果,从数据中生成一颗回归树。
myDat = loadDataSet('F:\python\machinelearninginaction\Ch09\ex00.txt') myMat = mat(myDat) print(createTree(myMat))
{'spInd': 0, 'spVal': 0.48813, 'left': 1.0180967672413792, 'right': -0.04465028571428572}
myDat1 = loadDataSet('F:\python\machinelearninginaction\Ch09\ex0.txt') myMat1 = mat(myDat1) print(createTree(myMat1))
{'spInd': 1, 'spVal': 0.39435, 'left': {'spInd': 1, 'spVal': 0.582002, 'left': {'spInd': 1, 'spVal': 0.797583, 'left': 3.9871632, 'right': 2.9836209534883724}, 'right': 1.980035071428571}, 'right': {'spInd': 1, 'spVal': 0.197834, 'left': 1.0289583666666666, 'right': -0.023838155555555553}}
现在已经完成了回归树的构建,下面将介绍树剪枝技术,它通过对决策树剪枝来达到更好的预测效果。
树剪枝
通过降低决策树的复杂度来避免过拟合的过程称为剪枝。
预剪枝这里不多做介绍敏著要讨论一下更理想化的剪枝方法-后剪枝
伪代码如下:
基于已有的树切分测试数据:
如果存在任意子集市一棵树,则在该子集递归剪枝过程
计算当前两个叶节点合并后的误差
计算不合并的误差
如果合并会降低误差的话,就将叶节点合并
def isTree(obj):
import types
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 = np.sum(np.power(lSet[:, -1] - tree['left'], 2)) + np.sum(
np.power(rSet[:, -1] - tree['right'], 2))
# 计算合并的均值
treeMean = (tree['left'] + tree['right']) / 2.0
# 计算合并的误差
errorMerge = np.sum(np.power(testData[:, -1] - treeMean, 2))
if errorMerge < errorNoMerge:
return treeMean
else:
return tree
else:
return tree
'''
有两个参数:待剪枝的树和剪枝所需的测试数据集
'''
myDat = loadDataSet('F:\python\machinelearninginaction\Ch09\ex00.txt')
myMat = mat(myDat)
#print(createTree(myMat))
myDat1 = loadDataSet('F:\python\machinelearninginaction\Ch09\ex0.txt')
myMat1 = mat(myDat1)
#print(createTree(myMat1))
myMat2 = loadDataSet('F:\python\machinelearninginaction\Ch09\ex2.txt')
myTree = createTree(myMat2,ops=(0,1))
#为了创建所有可能中最大的树
#输入以下命令导入测试数据:
myDatTest = loadDataSet('F:\python\machinelearninginaction\Ch09\ex2test.txt')
myMat2Test = mat(myDatTest)
#输入以下命令执行剪枝过程
prune(myTree,myMat2Test)
出现报错:TypeError: list indices must be integers or slices, not tuple
我服了,跟书上一样也不对,copy网上的代码也不对,为啥人家能运行出来啊,我真是无语。
找不出来为啥,放弃
找出来为啥了,将
mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0], :][0] mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :][0]
修改为:
mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0], :] mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :]
再将
fltLine = map(float, curLine)
修改为:
fltLine = list(map(float, curLine))
即可。
模型树
用树来建模还可以将叶节点设定为分段线性函数,这里所谓的分段线性是指模型由多个先行片段组成。这里需要给出每次切分适用于误差计算的代码,回归树将createTree()函数中的两个参数固定,这里略作修改,将前面的代码用于模型树。
下一个问题就是为了找到最佳切分如何去计算误差,这里对于给定的数据集,先用线性的模型来对她进行拟合,然后计算真是的目标值与模型预测值间的差值。最后将这些差值的【平方求和就得到了所需误差。
def linearSolve(dataSet): #该函数会被其他两个函数调用
m,n = shape(dataSet)
X = mat(ones((m,n))); Y = mat(ones((m,1)))#将数据集格式化成目标变量Y和自变量X
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):#当数据不再需要切分时,生成叶节点的模型,在数据集上调用linearSolve()并返回系数ws
ws,X,Y = linearSolve(dataSet)
return ws
def modelErr(dataSet):
ws,X,Y = linearSolve(dataSet)
yHat = X * ws
return sum(power(Y - yHat,2))
#在给定数据集上计算误差,会被chooseBestSplit()调用来找到最佳的切分
#该函数在数据集上调用linearSolve(),之后返回yHat、xHat
myMat2 = mat(loadDataSet('F:\python\machinelearninginaction\Ch09\exp2.txt'))
print(createTree(myMat2,modelLeaf,modelErr,(1,10)))
{'spInd': 0, 'spVal': 0.285477, 'left': matrix([[1.69855694e-03],
[1.19647739e+01]]), 'right': matrix([[3.46877936],
[1.18521743]])}
该代码以0.285477为界创建了两个模型,为了比较与其他模型哪一种更好,一个比较客观的方法是计算相关系数,也称为R^2值。该相关系数可以通过调用NumPy库中的命令corrcoef(yHat,y,rowvar=0)来求解,其中yHat是预测值,y是目标变量的实际值。
示例:树回归与标准回归的比较
def regTreeEval(model, inDat):
'''
:param model:
:param inDat:
:return:
该函数作为对回归树叶节点进行预测时调用
'''
return float(model)
def modelTreeEval(model, inDat):
'''
:param model:
:param inDat:
:return:
要对模型树节点进行预测时调用。这两个函数会对输入数据进行格式化处理
在元数据矩阵上增加第0列,然后计算并返回预测值。
'''
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):
'''
:param tree:
:param inData:
:param modelEval: 对叶节点数据进行预测的函数的引用
:return:
对于输入的单个数据点或者行向量,该函数返回一个浮点值。在给定树的情况下
,对于单个数据点,该函数会给出一个预测值。调用该函数时需要指定树的类型。
该函数自顶向下遍历整棵树,直到命中叶节点为止,一旦到达,他就会在输入数据上调用modelEval()函数
'''
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):
'''
:param tree:
:param testData:
:param modelEval:
:return:
多次调用treeForeCast()函数。它能够以向量形式返回一组预测值。
'''
m=len(testData)
yHat = mat(zeros((m,1)))
for i in range(m):
yHat[i,0] = treeForeCast(tree, mat(testData[i]), modelEval)
return yHat
接下来构建三个模型。
trainMat = mat(loadDataSet('F:\python\machinelearninginaction\Ch09\\bikeSpeedVsIq_train.txt'))
testMat = mat(loadDataSet('F:\python\machinelearninginaction\Ch09\\bikeSpeedVsIq_test.txt'))
myTree = createTree(trainMat,ops=(1,20))
yHat = createForeCast(myTree,testMat[:,0])
print(corrcoef(yHat,testMat[:,1],rowvar = 0)[0,1])
#利用该数据创建一颗回归树
print("---------------------------------")
#接下来创建一颗模型树
myTree = createTree(trainMat,modelLeaf,modelErr,(1,20))
yHat = createForeCast(myTree,testMat[:,0],modelTreeEval)
print(corrcoef(yHat,testMat[:,1],rowvar=0)[0,1])
0.964085231822215
---------------------------------
0.9760412191380629
从这里的结果可以看出,这里模型树的结果比回归树好,下面再看一下标准的线性回归效果如何。
ws,X,Y = linearSolve(trainMat)
print(ws)
[[37.58916794]
[ 6.18978355]]
为了得到测试集上所有的yHat的预测值,在测试数据上循环执行:
for i in range(shape(testMat)[0]): yHat[i] = testMat[i,0]*ws[1,0]+ws[0,0] print(corrcoef(yHat,testMat[:,1],rowvar=0)[0,1])
0.9434684235674766
可以看到树回归方法在预测复杂数据时会比简单的线性模型更加有效。
使用Python的Tkinter库创建GUI
在Python3中,tkinter的t要小写
from tkinter import *
root = Tk()#出现小窗口
myLabel = Label(root,text="Hello World")
myLabel.grid()#显示文字
root.mainloop()#该条命令启动事件循环,使窗口在众多事件中可以响应鼠标点击等动作
tkinter的GUI由一些 小部件组成,文本框、按钮,标签,复选按钮等。调用.grid()方法时,就是把myLabel的位置发告诉了布局管理器。该方法会把小部件安排咋该一个二维表格中。用户可以设定每个小部件所在的行列位置。接下来构建一个树管理器。
from numpy import *
from tkinter import *
import regTrees2
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
def reDraw(tolS, tolN):
'''
:param tolS:
:param tolN:
:return:
主要目的是把树绘制出来。该函数假定输入是合法的,首先清空之前的图像,使得前后两个图像不会重叠
清空图像时各个子图也都会被清除,所以需要重新添加一个图
接下来函数会检查复选框是否被选中,根据是否被选中来确定基于tolS和tilN参数
构建模型树还是回归树。
当构建完成后对测试机testDat进行预测,最后真实数据和预测值都会被绘制出来
具体操作是真实值采用scatter()方法绘制,预测值采用plot()方法绘制
前者构建的是离散型散点图,后者构建连续曲线
'''
reDraw.f.clf()
reDraw.a = reDraw.f.add_subplot(111)
if chkBtnVar.get():
if tolN < 2: tolN = 2
myTree = regTrees2.createTree(reDraw.rawDat, regTrees2.modelLeaf,
regTrees2.modelErr, (tolS, tolN))
yHat = regTrees2.createForeCast(myTree, reDraw.testDat,
regTrees2.modelTreeEval)
else:
myTree = regTrees2.createTree(reDraw.rawDat, ops=(tolS, tolN))
yHat = regTrees2.createForeCast(myTree, reDraw.testDat)
reDraw.a.scatter(array(reDraw.rawDat[:, 0]), array(reDraw.rawDat[:, 1]), s=5) # use scatter for data set
reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0)
reDraw.canvas.draw()
def getInputs():
'''
:return:
试图理解用户的输入防止程序崩溃。
tolS期望的输入是浮点数,tolN期望的输入是整数。
使用了try:except:模式,如果可以把输入文本解析成整数就继续执行
不能识别就输出错误消息,同时清空输入框并恢复默认值
'''
try:
tolN = int(tolNentry.get())
except:
tolN = 10
print("enter Integer for tolN")
tolNentry.delete(0, END)
tolNentry.insert(0, '10')
try:
tolS = float(tolSentry.get())
except:
tolS = 1.0
print("enter Float for tolS")
tolSentry.delete(0, END)
tolSentry.insert(0, '1.0')
return tolN, tolS
def drawNewTree():
'''
:return:
在有人点击ReDraw按钮时就会调用该函数。实现两个功能:
1.调用getInputs()方法得到输入框的值
2.利用该值调用reDraw()方法生成一个图
'''
tolN, tolS = getInputs()
reDraw(tolS, tolN)
root = Tk()
#首先建立一个Tk类型的根部件然后插入标签
reDraw.f = Figure(figsize=(5, 4), dpi=100) # create canvas
reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)
reDraw.canvas.draw()
reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)#使用该方法设定行和列的位置,也可以通过设定columnspan和rowspan的值来告诉布局管理器是否允许一个小部件跨行或者跨列
Label(root, text="tolN").grid(row=1, column=0)
tolNentry = Entry(root)
tolNentry.grid(row=1, column=1)
tolNentry.insert(0, '10')
Label(root, text="tolS").grid(row=2, column=0)
tolSentry = Entry(root)
tolSentry.grid(row=2, column=1)
tolSentry.insert(0, '1.0')
Button(root, text="ReDraw", command=drawNewTree).grid(row=1, column=2, rowspan=3)
chkBtnVar = IntVar()
chkBtn = Checkbutton(root, text="Model Tree", variable=chkBtnVar)#为了读取Checkbutton的状态需要创建一个变量,也就是IntVar
chkBtn.grid(row=3, column=0, columnspan=2)
reDraw.rawDat = mat(regTrees2.loadDataSet('F:\python\machinelearninginaction\Ch09\sine.txt'))
#初始化一些与reDraw()关联的全局变量
reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)
root.mainloop()