机器学习实战(九)——树回归

机器学习实战(九)——树回归

一、复杂数据的局部性建模

第三章所使用的决策树只能够处理离散型数据,并且是贪心算法,不断选取特征进行切分,这样通常不能够做到全局最优

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()
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值