机器学习实战(九)——树回归
一、复杂数据的局部性建模
第三章所使用的决策树只能够处理离散型数据,并且是贪心算法,不断选取特征进行切分,这样通常不能够做到全局最优
而CART是另外一种树构建算法,使用二元切分来处理连续型变量,可以用来解决分类问题和回归问题
二、连续和离散型特征的树的构建
在树的构建过程中,有一个问题就是如何存储一棵树。在CART中由于采用的是二元切分,因此每一个特征切分为两颗子树,因此需要存储的信息主要有待切分的特征,待切分的特征值、左子树和右子树。在python中可以用字典来解决这个存储的问题。
先实现一部分CART的代码:
from numpy import *
# 下载并处理数据
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float,curLine)) # python3这里map返回迭代器,需要加上list
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], :]
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 # 如果None说明满足了停止条件,这里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
三、将CART用于回归
首先要明确,叶节点的建模方式决定着整个树的结构与意义。回归树假设叶节点是常数值,这认为数据中各种复杂的关系可以用树的结构来概括。而类似于前面的决策树,我们在构建回归树的过程中同样需要一个指标来衡量连续性数据的混乱度,这里采用总方差
3.1、构建树
首先需要补充前面代码所缺少的 chooseBestSplit() 函数,它需要帮我们找到最佳的二元切分方式,同时还需要判断是否应该停止切分;这其中, l e a f T y p e leafType leafType是对创建叶节点的函数的引用,而 e r r T y p e errType errType是用于计算总方差的函数的引用, o p s ops ops则是包含一些终止计算的条件,具体如下:
def regLeaf(dataSet):
# 该函数对数据最后一列即结果进行求均值,用于在构建叶节点的时候调用,返回节点中样本结果的均值作为回归值
return mean(dataSet[:, -1])
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] # 如果切分出的数据很小,小于这个值也没意义
if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
# 集合的长度等于1说明当前节点中的样本都是都一个值,那么就不用在划分了
return None, leafType(dataSet)
m, n = shape(dataSet)
S = errType(dataSet)
bestS = inf # 用来存放最小的误差
bestIndex = 0 # 用来存放最好的切分特征
bestValue = 0 # 用来存放最好特征的切分值
for featIndex in range(n - 1): # 第n-1列为结果列
for splitVal in set(dataSet[:, featIndex].T.tolist()[0]): # 按照现有结果中的取值来划分
# 原文这里写错了,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):
# 这里判断的含义是防止前面根本找不到合适的切分点导致两个best都没有修改
return None, leafType(dataSet)
return bestIndex, bestValue
解释一下这行代码:
len(set(dataSet[:,-1].T.tolist()[0])) == 1
dataSet[:,-1] >>> 是取出结果那一列,出来一个m*1的矩阵
dataSet[:,-1].T >>> 转置,1*m
dataSet[:,-1].T.tolist() >>> 转成列表,因为上面是一个矩阵,因此是一个一个元素的列表,该元素是也是一个列表
dataSet[:,-1].T.tolist()[0] >>>取出其中的列表
可以通过例子更好地理解:
test = np.mat(np.eye((4)))
print(test)
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
test[:,-1].T
Out[7]: matrix([[0., 0., 0., 1.]])
test[:,-1].T.tolist()
Out[8]: [[0.0, 0.0, 0.0, 1.0]]
test[:,-1].T.tolist()[0]
Out[9]: [0.0, 0.0, 0.0, 1.0]
3.2、运行代码
接着就是通过打开两个文件"ex0.txt"和“ex00.txt”来验证程序的正确性。
这里附上画图的代码:
myData = regTree.loadDataSet("ex0.txt")
myMat = mat(myData)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(list(myMat.A[:,1]),list(myMat.A[:,2]))
plt.show()
四、树剪枝
4.1、预剪枝
上述算法存在一定的问题,就是如果样本数据中的某一个特征的数量级很大,那么其误差肯定也会是一个比较大的数量级,那么将导致误差很容易就大于 t o l S tolS tolS,那么就会很难满足停止切分的条件。也就是说 t o l S tolS tolS对样本的特征的数量级非常敏感
例如将上述第一个例子中的第二个特征值放大一百倍,这样在上图中我们仍然觉得可以分成两棵树,但是实际运行后将会对每一个样本都单独创建一个叶节点,因为误差的数量级过大,都超过了 t o l S tolS tolS
预剪枝则在运算之前提前观察数据并剪枝,例如此处我们观察到数据被放大了一百倍或者说数据的数量级过大,那么则手动修改 o p s ops ops的数值,但这每一次都要手动修改很麻烦,不太适合。
4.2、后剪枝
后剪枝的想法则是将数据集划分为训练集和测试集,然后在用训练集构建树的时候要让树尽可能大、尽可能复杂(叶结点尽可能多),然后再用测试集不断去判断这些叶结点是否能够进行合并来降低测试误差,从而选择是否剪枝。
具体函数为:
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))
# errorNoMerge就是根据test去划分然后计算误差
treeMean = (tree['left'] + tree['right']) / 2.0 # 如果不划分将直接取均值
# 这里应该是做简化处理,因为整体的均值并不是两个均值相差除以2,但前面也没办法获知每一个叶结点的数目
errorMetge = sum(power(testData[:, -1] - treeMean, 2))
# 不划分的时候的误差
if errorMetge < errorNoMerge:
print("Merging")
return treeMean
else:
return tree
else:
return tree
我运行这段代码时一直出现以下错误:
TypeError: list indices must be integers or slices, not tuple
发生在:
def binSplitDataSet(dataSet, feature, value):
mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0],:] # 这里不应该再加上[0],否则是取出第一行
mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0],:]
return mat0, mat1
它的意思是list类型的数据不能够通过元组去索引它,因此我做了如下修改:
def binSplitDataSet(dataSet, feature, value):
dataSet = mat(dataSet)
mat0 = dataSet[nonzero(dataSet[:, feature] > value)[0],:] # 这里不应该再加上[0],否则是取出第一行
mat1 = dataSet[nonzero(dataSet[:, feature] <= value)[0],:]
return mat0, mat1
将输入数据施加mat函数,保证其为矩阵类型可以用元组去索引
五、模型树
上述回归树是将叶节点设置为常数值,而另一个方法是将叶节点设置为分段线性函数,可以理解将某一部分数据分配到某一个叶节点符合一段线性函数,而另一部分数据分配到另一个叶节点符合另一段线性函数,如下图:
将较为平缓的那一段对应的样本点分配到一个斜率比较低的线性函数来拟合,而另一段的样本则分配到一个斜率比较大的线性函数来拟合,就能够有更好的拟合效果。
具体的实现方式加入如下代码:
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))
并在调用createTree时传入如下参数:
createTree(dataSet, leafType=modelLeaf, errType=modelErr, ops)
六、示例:树回归与标准回归的比较
前面介绍了模型树,回归树以及其他的回归方法,那么比较模型好坏的方法可以通过计算相关系数,即
R
2
R^2
R2值来体现。
这里补充简单的线性回归的代码:
def linearSolve(dataSet):
# 该函数用来进行简单的线性拟合,返回拟合出来的权值向量
m, n = shape(dataSet)
X = mat(ones((m,n)))
Y = mat(ones((m, 1)))
X[:, 1:n] = dataSet[:, 0:n - 1] # 第一列是1,其他与dataSet的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 regTreeEval(model,inDat):
# 这里传inDat的原因是为了和下面的函数统一格式
return float(model)
def modelTreeEval(model,inDat):
# 这里n不是训练集的n,测试集的列数比训练集少一列
n = shape(inDat)[1]
X = mat(ones((1,n+1))) # 因此这里是n+1,第一列用来放常数项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))) # 用来存放这m个样本的拟合值
for i in range(m):
yHat[i,0] = treeForeCast(tree,mat(testData[i]),modelEval)
return yHat
接下来我们就通过新的数据来比较回归树、模型树已经线性拟合三者的 R 2 R^2 R2值,从而来说明三个模型的好坏比较:
trainMat = mat(regTree.loadDataSet("bikeSpeedVsIq_train.txt"))
testMat = mat(regTree.loadDataSet("bikeSpeedVsIq_test.txt"))
myTree = regTree.createTree(trainMat,ops = (1,20))
yHat = regTree.createForeCast(myTree,testMat[:,0])
print(corrcoef(yHat,testMat[:,1],rowvar=0)[0,1])
myTree = regTree.createTree(trainMat,regTree.modelLeaf,regTree.modelErr,(1,20))
yHat = regTree.createForeCast(myTree,testMat[:,0],regTree.modelTreeEval)
print(corrcoef(yHat,testMat[:,1],rowvar=0)[0,1])
ws,X,Y = regTree.linearSolve(trainMat)
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.964085231822215
0.97604121913806
0.9434684235674767
这说明该数据在 R 2 R^2 R2的表现上,模型树最佳,其次是回归树,而线性拟合的表现相对来说是最差的。因此可以说明,树回归在拟合一些比较复杂的模型时比简单的线性模型表现更好。
七、使用Python的Tkinter库创建GUI
7.1、用Tkinter创建GUI
root = Tk()
myLabel = Label(root,text = 'Hello World')
myLabel.grid()
root.mainloop()
效果为:
下面为构建树管理器的代码:
from numpy import *
from tkinter import *
import regTree
def reDraw(tolS,tolN):
pass
def drawNewTree():
pass
root = Tk()
Label(root,text="Plot Place Holder").grid(row = 0,columnspan=3)
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)
chkBtn.grid(row=3,column = 0,columnspan=2)
reDraw.rawDat = mat(regTree.loadDataSet("D:\学习\大四上学习\Python机器学习代码实现\MLiA_SourceCode\machinelearninginaction\Ch09/sine.txt"))
reDraw.testDat = arange(min(reDraw.rawDat[:,0]),max(reDraw.rawDat[:,0]),0.01)
reDraw(1.0,10)
root.mainloop()
7.2、集成Matplotlib和Tkinter
from numpy import *
from tkinter import *
import regTree
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
def reDraw(tolS,tolN):
reDraw.f.clf()
reDraw.a = reDraw.f.add_subplot(111)
if chkBtnVar.get():
if tolN < 2:
tolN = 2
myTree = regTree.createTree(reDraw.rawDat,regTree.modelLeaf,\
regTree.modelErr,(tolS,tolN))
yHat = regTree.createForeCast(myTree,reDraw.testDat,\
regTree.modelTreeEval)
else:
myTree = regTree.createTree(reDraw.rawDat,ops = (tolS,tolN))
yHat = regTree.createForeCast(myTree,reDraw.testDat)
reDraw.a.scatter(reDraw.rawDat[:,0],reDraw.rawDat[:,1],s=5)
reDraw.a.plot(reDraw.testDat,yHat,linewidth=2.0)
reDraw.canvas.show()
def getInputs():
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():
tolN,tolS = getInputs()
reDraw(tolS,tolN)
root = Tk()
Label(root,text="Plot Place Holder").grid(row = 0,columnspan=3)
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)
chkBtn.grid(row=3,column = 0,columnspan=2)
reDraw.rawDat = mat(regTree.loadDataSet("D:\学习\大四上学习\Python机器学习代码实现\MLiA_SourceCode\machinelearninginaction\Ch09/sine.txt"))
reDraw.testDat = arange(min(reDraw.rawDat[:,0]),max(reDraw.rawDat[:,0]),0.01)
reDraw.f = Figure(figsize=(5,4),dpi = 100)
reDraw.canvas = FigureCanvasTkAgg(reDraw.f,master=root)
reDraw.canvas.show()
reDraw.canvas.get_tk_widget().grid(row = 0,columnspan = 3)
reDraw(1.0,10)
root.mainloop()