Part2-Chapter9-树回归

#本文代码引用函数:

import numpy as np
from tkinter import *
import regTrees
import matplotlib

matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

线性回归是一种一种非常强大的回归预测方法,然而它的问题在于,它在构建时需要拟合所有的数据点,这对一些复杂数据集来说,是非常难做到的。而且在现实生活中,问题通常都是非线性的,不可能用全局线性模型来拟合数据。

一种解决方法是先使用树将数据切分为几个易建模的数据,然后再使用回归来对其进行处理。

那么第一个问题是来构建树。在本书的第三章决策树里,我们使用了ID3算法来构建决策树,这一章里我们使用cart算法。二者的区别在于,ID3每次选择最好的特征来分割数据,且一个数据一旦在用于分割后,之后就不再使用。而cart算法使用了二元切分法来进行分割:如果数据的给定某特征的值大于给定区分值,则数据进入左子树,否则进入右子树(这里我觉得有点反直觉,可以借助反中序二叉树的模型来理解)。因此cart算法比起ID3算法的一个优势在于,它能够处理连续性数据。

构建代码如下:

#载入数据
def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curline = line.strip().split("\t")
        #将数据转化为浮点数组
        fltLine = list(map(float,curline))
        dataMat.append(fltLine)
    return dataMat

#划分左右树
#dataSet:数据集
#feature:划分特征
#value:划分值
def binSplitDataSet(dataSet,feature,value):
    #左子树
    mat0 = dataSet[np.nonzero(dataSet[:,feature]> value)[0],:]
    #右子树
    mat1 = dataSet[np.nonzero(dataSet[:,feature]<=value)[0],:]
    return mat0,mat1

#构建树
#dataSet:数据集
#leafType:节点构建函数的引用,默认为树回归
#errType:误差估计函数的引用,默认为树回归
#ops:输入的用来调节模型的参数,默认为(1,4)
def createTree(dataSet,leafType = regLeaf,errType = regErr,ops = (1,4)):
	#使用切分算法得到当前树划分所用的特征及划分值
    feat,val = chooseBestSplit(dataSet,leafType,errType,(1,4))
    #划分结束
    if feat == None:
        return val
        #根结点
    retTree = {}
    retTree['spInd'] = feat
    retTree['spVal'] = val
	
	#递归构建树
    lSet,rSet = binSplitDataSet(dataSet,feat,val)
    retTree['left'] = createTree(lSet)
    retTree['right'] = createTree(rSet)
    return retTree

构建树的关键在于如何选择划分的特征及特征值,cart算法的做法是遍历所有的特征的所有值,来找到使误差最小的那个特征及值,代码如下:

#寻找切分的最佳特征及值
#dataSet:数据集
#leafType:节点构建函数的引用,默认为树回归
#errType:误差估计函数的引用,默认为树回归
#ops:输入的用来调节模型的参数,默认为(1,4)
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
	#容许下降误差,误差变化高于该值才更新,避免出现震荡(我猜
    tolS = ops[0];
    #容许切分的最小数据数,高于该值才容许切分,减小奇异点的影响(我猜
    tolN = ops[1]
    # 停止切分的条件1:若剩余的不同特征数目=1时,则退出
    if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
        return None, leafType(dataSet)
    m, n = np.shape(dataSet)
    S = errType(dataSet)
    #初始化误差、切分特征及值
    bestS = np.inf;
    bestIndex = 0;
    bestValue = 0
    #遍历特征
    for featIndex in range(n - 1):
        #遍历特征值
        for splitVal in dataSet[:, featIndex]:
            mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)
            # 当切分的数据集小于切分的最小样本tolN时,则退出循环
            #在这里加入判断条件,增加了程序的复杂度,但是减少了时间复杂度
            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
    # 停止切分的条件2:若误差减小在tolS内,则退出
    if (S - bestS) < tolS:
        return None, leafType(dataSet)
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    # 停止切分的条件3:当切分的数据集小于切分的最小样本tolN时,则退出
    if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):
        return None, leafType(dataSet)
    #切分完成,返回特征及值
    return bestIndex, bestValue

另外还有两个函数regLeaf()和regErr(),分别用于生成叶节点及计算误差,代码如下:

#树回归的叶节点生成函数
#dataSet:数据集
def regLeaf(dataSet):
     eturn np.mean(dataSet[:, -1])

#树回归的误差估计函数
#dataSet:数据集
def regErr(dataSet):
    return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]

有时候,我们发现构建出来的树有过多的节点,那么我们认为该树可能对数据进行了过拟合。这是不符合我们预期的,为了减小过拟合现象,我们通过剪枝的方法来降低决策树的复杂度,剪枝有两种方法:预剪枝及后剪枝。

预剪枝:使用特定的变量来控制构建树的过程,从而避免过拟合的现象。这一方法的缺点在于给出的参数往往没有特定的依据,而是凭借经验和感觉给出,因此在处理一些复杂问题时,会非常繁琐且低效。其优点是,如果我们对数据有一定的期望,那么预剪枝可以帮助我们达到这个期望。

后剪枝:后剪枝需要划分训练集与测试集。首先我们给出一定的参数,来构建出足够复杂的决策树,以方便剪枝。然后从上至下地找到叶节点,并依次比较这些节点的误差与不合并的误差的大小,进而决定是否合并,是一个递归的过程。代码如下:

#判断是否为树
#obj:输入对象
def isTree(obj):
    import types
    return (type(obj).__name__ == 'dict')

#递归地计算左右节点的均值
#tree:输入的树
def getMean(tree):
    if isTree(tree['right']):tree['right'] = getMean(tree['right'])
    if isTree(tree['left']): tree['left'] = getMean(tree['left'])
    return (tree['right'] + tree['left']) / 2.0

#后剪枝
#tree为根据训练集训练出的待剪枝的树
#testData为测试集
def prune(tree,testData):
	#若无数据,则对树进行塌陷处理
    if np.shape(testData)[0] == 0:return getMean(tree)
    #若树的左子树有子树或右子树有子树,则得到测试集对应的数据,分别记为lSet和rSet
    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

用树来对数据建模时,除了将叶节点简单地设定为常数外,还可以设置为分段线性函数,使模型由多个线性函数组成,从而得到更好的拟合。这种方法称为模型树法。代码如下:

#格式化X,Y,并计算系数矩阵ws
#dataSet:数据集
def linearSolve(dataSet):
    m, n = np.shape(dataSet)
    # 初始化X,Y
    #X:数据矩阵
    X = np.mat(np.ones((m, n)))
    #Y:结果矩阵
    Y = np.mat(np.ones((m, 1)))  
    X[:, 1:n] = dataSet[:, 0:n - 1]
    Y = dataSet[:, -1]
    xTx = X.T * X
    #若无法求逆,报错
    if np.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

#模型树子节点构建函数
#dataSet:数据集
def modelLeaf(dataSet): 
    ws, X, Y = linearSolve(dataSet)
    return ws

#模型树误差估计函数
#dataSet:数据集
def modelErr(dataSet):
    ws, X, Y = linearSolve(dataSet)
    #yHat:函数预测值
    yHat = X * ws
    return np.sum(np.power(Y - yHat, 2))

上面我们了解了树回归、树模型两种方法,接下来我们将这两种方法与前面学过的普通线性回归进行比较。

首先构建一个使用树回归进行预测的函数,代码如下:

#回归树的预测函数
#model:叶节点数据
#inDat:数据集
def regTreeEval(model, inDat):
    return float(model)

#模型树的预测函数
#model:叶节点数据
#inDat:数据集
def modelTreeEval(model, inDat):
    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 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)

#树回归的预测函数
#tree:训练集构建的决策树
#testData:测试集
#modelEval:叶节点预测函数的引用
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

运行后计算各方法的预测值与真实值的平方差后发现,对于复杂的数据,树回归法比简单的线性回归更有效。

有时候我们需要直观地观察数据及预测地结果,以及直接与算法和数据交互。使用函数来解决这两个需求是比较麻烦的,因此我们使用绘图的方法来进行。

Tkinter是内嵌于python的一个GUI(图形用户界面(Graphical User Interface))框架。为了使用它,我们首先构建一个管理器界面,代码如下:

#绘图函数
#tolS:容许下降误差,误差高于该值才容许更新
#tolN:容许切分的最小数据数,高于该值才容许切分
def reDraw(tolS, tolN):
	#清屏
    reDraw.f.clf()  
    #绘制图表
    reDraw.a = reDraw.f.add_subplot(111)
    #若勾选了勾选框,则使用模型树法预测
    if chkBtnVar.get():
        if tolN < 2: tolN = 2
        myTree = createTree(reDraw.rawDat, modelLeaf, modelErr, (tolS, tolN))
        yHat = createForeCast(myTree, reDraw.testDat, modelTreeEval)
    #否则使用回归树法预测
    else:
        myTree = createTree(reDraw.rawDat, ops=(tolS, tolN))
        yHat = createForeCast(myTree, reDraw.testDat)
    reDraw.a.scatter(reDraw.rawDat[:, 0].A, reDraw.rawDat[:, 1].A, s=5)  
    reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0)  
    #绘制结果
    reDraw.canvas.draw()

#得到输入		
def getInputs():
     #检查tolN框有无输入
     try:
        tolN = int(tolNentry.get())
     #若无输入,则默认tolN为10
     except:
        tolN = 10
        print("enter Integer for tolN")
        tolNentry.delete(0, END)
        tolNentry.insert(0, '10')

     #检查tolS框有无输入
     try:
        tolS = float(tolSentry.get())
     #若无输入,则默认tolN为10
     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值
    tolN, tolS = getInputs() 
    reDraw(tolS, tolN)

#建立Tkinter类
root = Tk()
#建立文本框:Plot Place Holder'
Label(root,text='Plot Place Holder').grid(row = 0, columnspan = 3)
#建立文本框:tolN
Label(root, text='tolN').grid(row=1, column=0)
#实例化Entry控件
tolNentry = Entry(root)  
tolNentry.grid(row=1, column=1)
#默认tolN文本框输入10
tolNentry.insert(0, '10')  

#建立文本框:tolN
Label(root, text='tolS').grid(row=2, column=0)
#实例化Entry控件
tolSentry = Entry(root)
tolSentry.grid(row=2, column=1)
#默认tolN文本框输入1.0	
tolSentry.insert(0, '1.0') 

#建立Button控件,名为ReDraw,并设定点击函数为drawNewTree
Button(root, text='ReDraw', command = drawNewTree).grid(row=1, column=2, rowspan=3)

#实例化勾选框控件,名为Model Tree
chkBtnVar = IntVar()
chkBtn = Checkbutton(root, text='Model Tree', variable=chkBtnVar)
chkBtn.grid(row=3, column=0, columnspan=2)

#定义与reDraw()相关联的全局变量
reDraw.rawDat = np.mat(regTrees.loadDataSet('sine.txt'))
reDraw.testDat = np.arange(np.min(reDraw.rawDat[:, 0]), np.max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)

root.mainloop()

最终结果如下:
回归树:ops = 10,0.1
在这里插入图片描述
回归树:ops = 100,0.1
在这里插入图片描述
回归树:ops = 10,0.5
在这里插入图片描述
模型树:ops = 10,0.1
在这里插入图片描述
模型树:ops = 100,0.1
在这里插入图片描述
模型树:ops = 10,0.5
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值