[完]机器学习实战 第九章 树回归

一、本章内容

线性回归模型需要拟合所有样本(局部加权线性回归除外),当数据拥有众多特征且特征间关系复杂时,构建全局模型就显得太难了。一种可行的方法是将数据集切分成很多份易建模的数据,然后利用线性回归技术建模。如果首次切分后仍难以拟合线性模型就继续切分,在这种切分模式下,树结构和回归法相当有用。

CART(Classification And Regression Trees,分类回归树)算法,即可用于分类,也可用于回归。其中的树剪枝技术用于防止树的过拟合

决策树不断将数据切分成小数据集,直到所有目标变量完全相同,或者数据不能再切分为止。决策树是一种贪心算法,它要在给定的时间内做出最佳选择,但不关心能否达到全局最优。

前面介绍的决策树构建算法是ID3。ID3的做法是每次选取当前最佳的特征来分隔数据,并按照该特征的所有可能来切分。这种切分过于迅速,且不能处理连续性特征。另外一种方法是二元切分法,它易于对树构建过程进行调整以处理连续性特征。

CART是十分著名的树构建算法,它使用二元切分来处理连续性变量,对其稍作修改就可处理回归问题。CART算法也使用一个字典来存储树的数据结构,该字典含:

  • 待切分的特征
  • 待切分的特征值
  • 右子树,不需切分时,也可是单个值
  • 左子树,右子树类似

CART可构建两种书:回归树(regression tree),其每个叶节点包含单个值;模型树(model tree),其每个叶节点包含一个线性方程。创建树的函数createTree()的伪代码大致如下:

找到最佳的待切分特征 :
    如果该节点不能再分,将该节点存为叶节点
    执行二元切分
    在右子树调用createTree()方法
    在左子树调用createTree()方法

1-1 CART用于回归

回归树假设叶节点是常数值,这种策略认为数据中的复杂关系可用树结构来概括。为了构建以分段常数叶节点的树,需要度量数据的一致性。使用ID3算法构建的决策树进行分类,会在给定节点时计算数据的混乱度。计算连续性数值的混乱度是非常简单的,首先计算所有数值的均值,然后计算每条数据的值到均值的差值。为了对正负差值同等看待,一般使用绝对值或平方值来代替上述差值。这里使用的是总方差(平方误差的总值),总方差=均方差*样本数。

1-1-1 构建树

构建树首先要实现如何切分数据集,使用函数chooseBestSplit()函数切分数据集。给定误差计算方法,该函数寻找数据集上的最佳二元切分方式,一旦停止切分会生成一个叶节点。它遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。函数的伪代码如下:

对每个特征 : 
    对每个特征值 : 
        将数据集切分成两份
        计算切分的误差
        如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
返回最佳切分的特征和阈值

切分停止的三个条件:

  • 剩余特征值的数目为1
  • 如果切分数据集后的误差提升不大,不应进行切分操作,而直接创建叶节点
  • 两个切分后的子集中的一个的大小小于用户定义的参数tolN时

1-2 树剪枝

决策树也可使用测试集上某种交叉验证技术发现过拟合。通过降低决策树的复杂度来避免过拟合的过程称为剪枝pruning),在chooseBestSplit()中提前终止条件,实际上是一种所谓的预剪枝prepruning)操作。另一种剪枝需要使用测试集和训练集,称为后剪枝postpruning)。

树构建算法对输入参数tolS和tolN(预剪枝)非常敏感。停止条件tolS对误差的数量级十分敏感。通过不断修改停止条件来得到合理结果不是好办法,事实上,我们常常不确定需要寻找什么样的结果,而这正是机器学习所关注的内容,计算机应可给出总体的概貌。

后剪枝,使用测试集来对树进行剪枝,不需要用户指定参数,是一种更理想化的剪枝方法。使用后剪枝,需要将数据集分为测试集和训练集。首先指定参数,使得构建的树足够大和复杂,便于剪枝。接着,从上到下找到叶节点,用测试集来判断将这些叶节点合并是否能够降低测试误差,如果是的话就合并,合并也称为塌陷处理,在回归树中一般采用取需要合并的所有子树的平均值。函数prune()的伪代码如下:

基于已有的树切分测试数据 :
    如果存在任一子集是一棵树,则在该子集递归剪枝过程
    计算将当前两个叶节点合并后的误差
    计算不合并的误差
    如果合并会降低误差的话,那就将叶节点合并

后剪枝可能不如预剪枝有效,一般,为了需求最佳模型可同时使用两种剪枝技术。

1-3 模型树

用树建模,除了把叶节点简单地设定为常数值外,还可把叶节点设定为分段线性函数,这里的分段线性是指模型由多个线性片段组成。模型树的可解析性是它由于回归的特点之一,它还具有更高的预测准确度

模型树、回归树以及其他模型中那个更好,可使用相关系数 R2 来衡量。具体使用numpy.corrcoef(yHat, y, rowvar=0)来求解。

1-4 使用Python的Tkinter库创建GUI

  • 使用如下命令,会出现一个小窗口
>>> from Tkinter import *
>>> root = Tk()
  • 在窗口显示一些文字,输入如下命令
>>> myLabel = Label(root, text="Hello World")
>>> myLabel.grid()
  • 为了使程序完整,输入如下命令。此命令将启动事件循环,使窗口在众多事件中可以相应鼠标点击、按键和重绘等动作。
>>> root.mainloop()

Tkinter的GUI由一些小部件(Widget)组成。小部件,指的是文本框(TextBox)、按钮(Button)、标签(Label)和复选按钮(CheckButton)等对象。上例中myLabel的.grid()方法把myLabel的位置告诉了布局管理器。详细的实例见代码treeExplore.py

1-4-1 集成Matplotlib和Tkinter

可以将Matplotlib绘制的图像放在GUI上,通过修改Matplotlib后端达到在Tkinter的GUI上绘图的目的。Matplotlib的构建程序包含一个前端,即面向用户的一些代码,如plot()和scatter()方法。Matplotlib同时也创建了一个后端,用于实现绘图和不同应用间的接口。通过改变后端可将图像绘制在PNG、PDF、SVG等格式的文件上。

接下来,设置Matplotlib的后端为TkAgg(Agg是一个C++的库,可从图像创建光栅图),TkAgg可在所选的GUI框架上调用Agg,把Agg呈现在画布上。可在Tk的GUI上放置一个画布,并用.grid()来调整布局。

具体的集成代码,请参照treeExplore.py,treeExplore.py绘制的图形如下所示


默认的treeExplore图形用户界面,该界面同时显示了输入数据和一个回归树模型,其中参数tolN=10,tolS=1.0
图 默认的treeExplore图形用户界面,该界面同时显示了输入数据和一个回归树模型,其中参数tolN=10,tolS=1.0


用treeExplore的GUI构建的模型树,tolN=10,tolS=1.0。与回归树相比,模型树获得了更好的预测效果
图 用treeExplore的GUI构建的模型树,tolN=10,tolS=1.0。与回归树相比,模型树获得了更好的预测效果

1-5 使用的函数

函数 功能
map(function, sequence[, sequence, …]) -> list 根据提供的函数对指定序列做映射,简单的示例,map(float, curLine):对curLine中的元素作浮点转换
numpy.var(a) 计算样本集的方差
pow(x, y[, z]) 函数是计算x的y次方,如果z在存在,则再对结果进行取模,其结果等效于pow(x,y) %z,pow() 通过内置的方法直接调用,内置方法会把参数作为整型,而 math 模块则会把参数转换为 float。
numpy.linalg.det(xTx) 求方阵的行列式
xTx.I 对矩阵求逆
corrcoef(yHat, y, rowvar=0) 求解yHat,y的相关系数,也称为 R2

二、程序代码

2-1 regTrees.py

# coding=utf-8

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

# dataSet: 数据集合
# feature: 待切分的特征
# value: 该特征的某个值    
def binSplitDataSet(dataSet, feature, value) :
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
    return mat0, mat1

# 负责生成叶节点,当chooseBestSplit()函数确定不再对数据进行切分时,
# 将调用该regLeaf()函数来得到叶节点的模型,在回归树中,该模型其实就是目标变量的均值
def regLeaf(dataSet) :
    return mean(dataSet[:, -1])

# 误差估计函数,该函数在给定的数据上计算目标变量的平方误差,这里直接调用均方差函数var
# 因为这里需要返回的是总方差,所以要用均方差乘以数据集中样本的个数  
def regErr(dataSet) :
    return var(dataSet[:, -1]) * shape(dataSet)[0]

# dataSet: 数据集合
# leafType: 给出建立叶节点的函数
# errType: 误差计算函数
# ops: 包含树构建所需其他参数的元组
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)) :
    # 将数据集分成两个部分,若满足停止条件,chooseBestSplit将返回None和某类模型的值
    # 若构建的是回归树,该模型是个常数。若是模型树,其模型是一个线性方程。
    # 若不满足停止条件,chooseBestSplit()将创建一个新的Python字典,并将数据集分成两份,
    # 在这两份数据集上将分别继续递归调用createTree()函数
    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

# 回归树的切分函数,构建回归树的核心函数。目的:找出数据的最佳二元切分方式。如果找不到
# 一个“好”的二元切分,该函数返回None并同时调用createTree()方法来产生叶节点,叶节点的
# 值也将返回None。
# 如果找到一个“好”的切分方式,则返回特征编号和切分特征值。
# 最佳切分就是使得切分后能达到最低误差的切分。
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)) :
    # tolS是容许的误差下降值
    # tolN是切分的最小样本数
    tolS = ops[0]; tolN = ops[1]
    # 如果剩余特征值的数目为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
    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)
    # 检查切分后的子集大小,如果某个子集的大小小于用户定义的参数tolN,那么也不应切分。
    if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN) :
        return None, leafType(dataSet)
    # 如果前面的这些终止条件都不满足,那么就返回切分特征和特征值。
    return bestIndex, bestValue

# 主要功能:将数据格式化成目标变量Y和自变量X。X、Y用于执行简单的线性规划。
def linearSolve(dataSet) :
    m,n = shape(dataSet) 
    X = mat(ones((m,n))); Y = mat(ones((m,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, \n try increasing the second value of ops')
    ws = xTx.I * (X.T * Y)
    return ws, X, Y

# 与regLeaf()类似,当数据不需要切分时,它负责生成叶节点的模型。
def modelLeaf(dataSet) :
    ws, X, Y = linearSolve(dataSet)
    return ws

# 在给定的数据集上计算误差。与regErr()类似,会被chooseBestSplit()调用来找到最佳切分。
def modelErr(dataSet) :
    ws, X, Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(power(Y-yHat, 2))


# 为了和modeTreeEval()保持一致,保留两个输入参数
def regTreeEval(model, inDat) :
    return float(model)

# 对输入数据进行格式化处理,在原数据矩阵上增加第0列,元素的值都是1
def modelTreeEval(model, inDat) :
    n = shape(inDat)[1]
    X = mat(ones((1, n+1)))
    X[:, 1:n+1] = inDat
    return float(X*model)

# 在给定树结构的情况下,对于单个数据点,该函数会给出一个预测值。
# modeEval是对叶节点进行预测的函数引用,指定树的类型,以便在叶节点上调用合适的模型。
# 此函数自顶向下遍历整棵树,直到命中叶节点为止,一旦到达叶节点,它就会在输入数据上
# 调用modelEval()函数,该函数的默认值为regTreeEval()
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)

# 多次调用treeForeCast()函数,以向量形式返回预测值,在整个测试集进行预测非常有用
def createForeCast(tree, testData, modelEval=regTreeEval) :
    m = len(testData)
    yHat = mat(zeros((m,1)))
    for i in range(m) :
        yHat[i,0] = treeForeCast(tree, mat(testData[i]), modelEval)
    return yHat

2-2 treeExplore.py

# coding=utf-8

from numpy import *
from Tkinter import *
import ml.regTrees as regTrees

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 = regTrees.createTree(reDraw.rawDat, regTrees.modelLeaf, regTrees.modelErr, (tolS, tolN))
        yHat = regTrees.createForeCast(myTree, reDraw.testDat, regTrees.modelTreeEval)
    else :
        myTree = regTrees.createTree(reDraw.rawDat, ops=(tolS, tolN))
        yHat = regTrees.createForeCast(myTree, reDraw.testDat)
    # reDraw.rawDat[:,0].A,需要将矩阵转换成数组
    reDraw.a.scatter(reDraw.rawDat[:,0].A, reDraw.rawDat[:,1].A, 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()
    # 利用tolN,tolS,调用reDraw生成漂亮的图
    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')
# 点击“ReDraw”按钮后,调用drawNewTree()函数
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.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.rawDat = mat(regTrees.loadDataSet('c:\python27\ml\\sine.txt'))
reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)

reDraw(1.0, 10)

root.mainloop()

三、命令行中执行

>>> import ml.regTrees
>>> from numpy import *
>>> testMat = mat(eye(4))
>>> testMat
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  1.,  0.,  0.],
        [ 0.,  0.,  1.,  0.],
        [ 0.,  0.,  0.,  1.]])
>>> mat0, mat1 = regTrees.binSplitDataSet(testMat, 1, 0.5)
>>> mat0
matrix([[ 0.,  1.,  0.,  0.]])
>>> mat1
matrix([[ 1.,  0.,  0.,  0.],
        [ 0.,  0.,  1.,  0.],
        [ 0.,  0.,  0.,  1.]])
# 运行二元切分代码
>>> reload(regTrees)
<module 'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> myData = regTrees.loadDataSet('c:\python27\ml\\ex00.txt')
>>> myMat = mat(myData)
>>> regTrees.createTree(myMat)
{
   
   'spInd': 0, 'spVal': matrix([[ 0.48813]]), 'right': -0.044650285714285719, 'left': 1.0180967672413792}
# 多次切分的列子
>>> myData1 = regTrees.loadDataSet('c:\python27\ml\\ex0.txt')
>>> myMat1 = mat(myData1)
>>> regTrees.createTree(myMat1)
{
   
   'spInd': 1, 'spVal': matrix([[ 0.39435]]), 'right': {
   
   'spInd': 1, 'spVal': matri
x([[ 0.197834]]), 'right': -0.023838155555555553, 'left': 1.0289583666666666}, '
left': {
   
   'spInd': 1, 'spVal': matrix([[ 0.582002]]), 'right': 1.980035071428571,
'left': {
   
   'spInd': 1, 'spVal': matrix([[ 0.797583]]), 'right': 2.9836209534883724
, 'left': 3.9871631999999999}}}
# 后剪枝
>>> reload(regTrees)
<module 'ml.regTrees' from 'C:\Python27\ml\regTrees.pyc'>
>>> myData2 = regTrees.loadDataSet('c:\python27\ml\\ex2.txt')
>>> myMat2 = mat(myData2)
>>> from numpy import *
>>> myMat2 = mat(myData2)
>>> regTrees.createTree(myMat2)
>>> regTrees.createTree(myMat2)
{
   
   'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': {
   
   'spInd': 0, 'spVal': matr
......
0, 'spVal': matrix([[ 0.958512]]), 'right': 112.42895575000001, 'left': 105.2486
2350000001}}}}
>>> regTrees.createTree(myMat2, ops=(10000, 4))
{
   
   'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': -2.6377193297872341, 'left
': 101.35815937735848}
>>> myTree = regTrees.createTree(myMat2, ops=(0,1))
>>> myDataTest = regTrees.loadDataSet('c:\python27\ml\\ex2test.txt')
>>> myMat2Test = mat(myDataTest)
>>> regTrees.prune(myTree, myMat2Test)
Merging
Merging
Merging
......
Merging
{
   
   'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': {
   
   'spInd': 0, 'spVal': matr
......
rix([[ 0.965969]]), 'right': {
   
   'spInd': 0, 'spVal': matrix([[ 0.956951]]), 'right
': 111.2013225, 'left': {
   
   'spInd': 0, 'spVal': matrix([[ 0.958512]]), 'right': 13
5.83701300000001, 'left': {
   
   'spInd': 0, 'spVal': matrix([[ 0.960398]]), 'right':
123.559747, 'left': 112.386764}}}, 'left': 92.523991499999994}}}}
# 模型树
>>> reload(regTrees)
<module 'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> myMat2 = mat(regTrees.loadDataSet('c:\python27\ml\\exp2.txt'))
>>> regTrees.createTree(myMat2, regTrees.modelLeaf, regTrees.modelErr, (1,10))
{
   
   'spInd': 0, 'spVal': matrix([[ 0.285477]]), 'right': matrix([[ 3.46877936],
        [ 1.18521743]]), 'left': matrix([[  1.69855694e-03],
        [  1.19647739e+01]])}
# 树回归与标准回归的比较
>>> reload(regTrees)
<module 'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> trainMat = mat(regTrees.loadDataSet('c:\python27\ml\\bikeSpeedVsIq_train.txt'))
>>> testMat = mat(regTrees.loadDataSet('c:\python27\ml\\bikeSpeedVsIq_test.txt'))
>>> myTree = regTrees.createTree(trainMat, ops=(1,20))
>>> yHat = regTrees.createForeCast(myTree, testMat[:,0])
>>> corrcoef(yHat, testMat[:,1], rowvar=0)
array([[ 1.        ,  0.96408523],
       [ 0.96408523,  1.        ]])
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.96408523182221384
>>> myTree = regTrees.createTree(trainMat, regTrees.modelLeaf, regTrees.modelErr
, (1,20))
>>> yHat = regTrees.createForeCast(myTree, testMat[:,0], regTrees.modelTreeEval)
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.97604121913806285
# 标准回归
>>> ws, X, Y = regTrees.linearSolve(trainMat)
>>> ws
matrix([[ 37.58916794],
        [  6.18978355]])
>>> for i in range(shape(testMat)[0]) :
...     yHat[i] = testMat[i,0]*ws[1,0] + ws[0,0]
...
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.94346842356747584
# 用Tkinter创建GUI
>>> from Tkinter import *
>>> root = Tk()
>>> myLabel = Label(root, text="Hello World")
>>> myLabel.grid()
# 为了程序的完整,还应输入下面命令,此命令将启动时间循环,使该窗口在众多事件中可以响应鼠标点击、按键和重绘动作
>>> root.mainloop()

猜你喜欢

转载自blog.csdn.net/namelessml/article/details/52595066