树回归CART

之前线性回归创建的模型需要拟合所有的样本点,但数据特征众多,关系复杂时,构建全局模型就很困难。之前构建决策树使用的算法是ID3。

ID3 的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有 4 种取值,那么数据将被切分成 4 份。一旦按照某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。另外一种方法是二元切分法,即每次把数据集切分成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入树的右子树。

除了切分过于迅速外, ID3 算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征转换成离散型,才能在 ID3 算法中使用。但这种转换过程会破坏连续型变量的内在性质。而使用二元切分法则易于对树构造过程进行调整以处理连续型特征。具体的处理方法是: 如果特征值大于给定值就走左子树,否则就走右子树。另外,二元切分法也节省了树的构建时间,但这点意义也不是特别大,因为这些树构建一般是离线完成,时间并非需要重点关注的因素。

CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量。对 CART 稍作修改就可以处理回归问题。决策树中使用香农熵来度量集合的无组织程度。如果选用其他方法来代替香农熵,就可以使用树构建算法来完成回归。

回归树与分类树的思路类似,但是叶节点的数据类型不是离散型,而是连续型。

还有一点要说明,构建决策树算法,常用到的是三个方法: ID3, C4.5, CART.
三种方法区别是划分树的分支的方式:

  1. ID3 是信息增益分支
  2. C4.5 是信息增益率分支
  3. CART 做分类工作时,采用 GINI 值作为节点分裂的依据;回归时,采用样本的最小方差作为节点的分裂依据。

工程上总的来说:

CART 和 C4.5 之间主要差异在于分类结果上,CART 可以回归分析也可以分类,C4.5 只能做分类;C4.5 子节点是可以多分的,而 CART 是无数个二叉子节点;

以此拓展出以 CART 为基础的 “树群” Random forest , 以 回归树 为基础的 “树群” GBDT 。

树剪枝

一棵树如果节点过多,表明该模型可能对数据进行了 “过拟合”。

通过降低决策树的复杂度来避免过拟合的过程称为 剪枝(pruning)。在函数 chooseBestSplit() 中提前终止条件,实际上是在进行一种所谓的 预剪枝(prepruning)操作。另一个形式的剪枝需要使用测试集和训练集,称作 后剪枝(postpruning)

预剪枝(prepruning)

顾名思义,预剪枝就是及早的停止树增长,在构造决策树的同时进行剪枝。

所有决策树的构建方法,都是在无法进一步降低熵的情况下才会停止创建分支的过程,为了避免过拟合,可以设定一个阈值,熵减小的数量小于这个阈值,即使还可以继续降低熵,也停止继续创建分支。但是这种方法实际中的效果并不好。

后剪枝(postpruning)

决策树构造完成后进行剪枝。剪枝的过程是对拥有同样父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否小于某一阈值。如果确实小,则这一组节点可以合并一个节点,其中包含了所有可能的结果。合并也被称作 塌陷处理 ,在回归树中一般采用取需要合并的所有子树的平均值。后剪枝是目前最普遍的做法。

模型树

用树来对数据建模,除了把叶节点简单地设定为常数值之外,还有一种方法是把叶节点设定为分段线性函数,这里所谓的 分段线性(piecewise linear) 是指模型由多个线性片段组成。

from __future__ import print_function
from Tkinter import *
from numpy import *

# 默认解析的数据是用tab分隔,并且是数值类型
# general function to parse tab -delimited floats
def loadDataSet(fileName):
    """loadDataSet(解析每一行,并转化为float类型)
        Desc: 该函数读取一个以 tab 键为分隔符的文件,然后将每行的内容保存成一组浮点数
    Args:
        fileName 文件名
    Returns:
        dataMat 每一行的数据集array类型
    Raises:
    """
    # 假定最后一列是结果值
    # assume last column is target value
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        # 将所有的元素转化为float类型
        # map all elements to float()
        # map() 函数具体的含义,可见 https://my.oschina.net/zyzzy/blog/115096
        fltLine = map(float, curLine)
        dataMat.append(fltLine)
    return dataMat

def binSplitDataSet(dataSet, feature, value):
    """binSplitDataSet(将数据集,按照feature列的value进行 二元切分)
        Description: 在给定特征和特征值的情况下,该函数通过数组过滤方式将上述数据集合切分得到两个子集并返回。
    Args:
        dataMat 数据集
        feature 待切分的特征列
        value 特征列要比较的值
    Returns:
        mat0 小于等于 value 的数据集在左边
        mat1 大于 value 的数据集在右边
    Raises:
    """
    # # 测试案例
    # print 'dataSet[:, feature]=', dataSet[:, feature]
    # print 'nonzero(dataSet[:, feature] > value)[0]=', nonzero(dataSet[:, feature] > value)[0]
    # print 'nonzero(dataSet[:, feature] <= value)[0]=', nonzero(dataSet[:, feature] <= value)[0]

    # dataSet[:, feature] 取去每一行中,第1列的值(从0开始算)
    # nonzero(dataSet[:, feature] > value)  返回结果为true行的index下标
    mat0 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :]
    mat1 = dataSet[nonzero(dataSet[:, feature] > value)[0], :]
    return mat0, mat1

# 返回每一个叶子结点的均值
# returns the value used for each leaf
# 我的理解是: regLeaf 是产生叶节点的函数,就是求均值,即用聚类中心点来代表这类数据
def regLeaf(dataSet):
    return mean(dataSet[:, -1])

# 计算总方差=方差*样本数
# 我的理解是: 求这组数据的方差,即通过决策树划分,可以让靠近的数据分到同一类中去
def regErr(dataSet):
    # shape(dataSet)[0] 表示行数
    return var(dataSet[:, -1]) * shape(dataSet)[0]

# 1.用最佳方式切分数据集
# 2.生成相应的叶节点
def chooseBestSplit(dataSet,leafType=regLeaf,errType=regErr,ops=(1,4)):
    '''
    chooseBestSplit(用最佳方式切分数据集 和 生成相应的叶节点)

    Args:
        dataSet   加载的原始数据集
        leafType  建立叶子点的函数
        errType   误差计算函数(求总方差)
        ops       [容许误差下降值,切分的最少样本数]。
    Returns:
        bestIndex feature的index坐标
        bestValue 切分的最优值
    Raises:
    '''
    #ops=(1,4),非常重要,因为它决定了决策树划分停止的threshold值,被称为预剪枝(prepruning),其实也就是用于控制函数的停止时机。
    # 之所以这样说,是因为它防止决策树的过拟合,所以当误差的下降值小于tolS,或划分后的集合size小于tolN时,选择停止继续划分。
    # 最小误差下降值,划分后的误差减小小于这个差值,就不用继续划分
    tolS=ops[0]
    # 划分最小 size 小于,就不继续划分了
    tolN=ops[1]
    # 如果结果集(最后一列为1个变量),就返回退出
    # .T 对数据集进行转置
    # .tolist()[0] 转化为数组并取第0列
    if len(set(dataSet[:,-1].T.tolist()[0]))==1:# 如果集合size为1,也就是说全部的数据都是同一个类别,不用继续划分。
        #  exit cond 1
        return None,leafType(dataSet)
    # 计算行列值
    m,n=shape(dataSet)
    # 无分类误差的总方差和
    # the choice of the best feature is driven by Reduction in RSS error from mean
    S=errType(dataSet)
    # inf 正无穷大
    bestS, bestIndex, bestValue = inf, 0, 0
    # 循环处理每一列对应的feature值
    for featIndex in range(n-1): # 对于每个特征
        # [0]表示这一列的[所有行],不要[0]就是一个array[[所有行]],下面的一行表示的是将某一列全部的数据转换为行,然后设置为list形式
        for splitVal in set(dataSet[:,featIndex].T.tolist()[0]):
            # 对该列进行分组,然后组内成员的val值进行二元切分
            mat0,mat1=binSplitDataSet(dataSet,featIndex,splitVal)
            # 判断二元切分的方式的元素数量是否符合预期
            if(shape(mat0)[0]", yHat[i, 0]
    return yHat
# 测试数据集
testMat=mat(eye(4))
print(testMat)
print(type(testMat))
mat0,mat1=binSplitDataSet(testMat,1,0.5)
print(mat0,'\n-----------\n', mat1)

# 回归树
myDat=loadDataSet('data/9.RegTrees/data1.txt')
myMat=mat(myDat)
myTree=createTree(myMat)
print (myTree)

#1.预剪枝就是: 提起设置最大误差数和最少元素数
myDat = loadDataSet('data/9.RegTrees/data3.txt')
myMat=mat(myDat)
myTree=createTree(myMat,ops=(0,1))
print (myTree)

# 2. 后剪枝就是: 通过测试数据,对预测模型进行合并判断
myDatTest = loadDataSet('data/9.RegTrees/data3test.txt')
myMat2Test = mat(myDatTest)
myFinalTree = prune(myTree, myMat2Test)
print ('\n\n\n-------------------')
print (myFinalTree)

# 模型树求解
myDat = loadDataSet('data/9.RegTrees/data4.txt')
myMat = mat(myDat)
myTree = createTree(myMat, modelLeaf, modelErr)
print (myTree)

# 回归树 VS 模型树 VS 线性回归
trainMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_train.txt'))
testMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_test.txt'))
# 回归树
myTree1 = createTree(trainMat, ops=(1, 20))
print (myTree1)
yHat1 = createForeCast(myTree1, testMat[:, 0])
print ("--------------\n")
print (yHat1)
print ("ssss==>", testMat[:, 1])
# corrcoef 返回皮尔森乘积矩相关系数
# 描述XY相关程度
print("regTree:",corrcoef(yHat1,testMat[:,1],rowvar=0)[0,1])

# 模型树
myTree2 = createTree(trainMat, modelLeaf, modelErr, ops=(1, 20))
yHat2 = createForeCast(myTree2, testMat[:, 0], modelTreeEval)
print( myTree2)
print ("modelTree:", corrcoef(yHat2, testMat[:, 1],rowvar=0)[0, 1])

# 线性回归
ws, X, Y = linearSolve(trainMat)
print (ws)
m = len(testMat[:, 0])
yHat3 = mat(zeros((m, 1)))
for i in range(shape(testMat)[0]):
    yHat3[i]=testMat[i,0]*ws[1,0]+ws[0,0]
print ("lr:", corrcoef(yHat3, testMat[:, 1],rowvar=0)[0, 1])

使用GUI

from __future__ import print_function
from Tkinter import *
from numpy import *

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


def test_widget_text(root):
    mylabel = Label(root, text="helloworld")
    # 相当于告诉 布局管理器(Geometry Manager),如果不设定位置,默认在 0行0列的位置
    mylabel.grid()


# 最大为误差, 最大子叶节点的数量
def reDraw(tolS, tolN):
    # clear the figure
    reDraw.f.clf()
    reDraw.a = reDraw.f.add_subplot(111)

    # 检查复选框是否选中
    if chkBtnVar.get():
        if tolN < 2:
            tolN = 2
        myTree = createTree(reDraw.rawDat, regTrees.modelLeaf, regTrees.modelErr, (tolS, tolN))
        yHat = createForeCast(myTree, reDraw.testDat, regTrees.modelTreeEval)
    else:
        myTree = createTree(reDraw.rawDat, ops=(tolS, tolN))
        yHat = createForeCast(myTree, reDraw.testDat)

    # use scatter for data set
    reDraw.a.scatter(reDraw.rawDat[:, 0].A, reDraw.rawDat[:, 1].A, s=5)
    # use plot for yHat
    reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0, c='red')
    reDraw.canvas.draw()


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


# 画新的tree
def drawNewTree():
    # #get values from Entry boxes
    tolN, tolS = getInputs()
    reDraw(tolS, tolN)


def main(root):
    # 标题
    Label(root, text="Plot Place Holder").grid(row=0, columnspan=3)
    # 输入栏1, 叶子的数量
    Label(root, text="tolN").grid(row=1, column=0)
    global tolNentry
    tolNentry = Entry(root)
    tolNentry.grid(row=1, column=1)
    tolNentry.insert(0, '10')
    # 输入栏2, 误差量
    Label(root, text="tolS").grid(row=2, column=0)
    global tolSentry
    tolSentry = Entry(root)
    tolSentry.grid(row=2, column=1)
    # 设置输出值
    tolSentry.insert(0,'1.0')

    # 设置提交的按钮
    Button(root, text="确定", command=drawNewTree).grid(row=1, column=2, rowspan=3)

    # 设置复选按钮
    global chkBtnVar
    chkBtnVar = IntVar()
    chkBtn = Checkbutton(root, text="Model Tree", variable = chkBtnVar)
    chkBtn.grid(row=3, column=0, columnspan=2)

    # 退出按钮
    Button(root, text="退出", fg="black", command=quit).grid(row=1, column=2)

    # 创建一个画板 canvas
    reDraw.f = Figure(figsize=(5, 4), dpi=100)
    reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)
    reDraw.canvas.draw()
    reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)

    reDraw.rawDat = mat(loadDataSet('data/9.RegTrees/sine.txt'))
    reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)
    reDraw(1.0, 10)
# 创建一个事件
root = Tk()
# test_widget_text(root)
main(root)
# 启动事件循环
root.mainloop()

你可能感兴趣的:(AI,回归,人工智能)