树回归

一、CART算法用于回归
1、原理
分类与回归树(CART)是应用广泛的决策树学习方法。CART由特征选择、树的生成及剪枝组成,既可以用于分类也可以用于回归。
算法如下:
输入:训练集 X X X
过程:
①寻找最优切分属性 j j j及其属性对应的切分值 s s s,求解:
min ⁡ j , s [ ∑ x i ∈ R 1 ( j , s ) ( y i − c 1 ) 2 + ∑ x i ∈ R 2 ( j , s ) ( y i − c 2 ) ] \min_{j,s}[\sum_{x_i∈R_1(j,s)}(y_i-c_1)^2+\sum_{x_i∈R_2(j,s)}(y_i-c_2)] j,smin[xiR1(j,s)(yic1)2+xiR2(j,s)(yic2)]
这里的 R 1 R_1 R1 R 2 R_2 R2是依据最优切分属性 j j j和切分值 s s s得到的两个切分后区域。 c 1 c_1 c1 c 2 c_2 c2 R 1 R_1 R1 R 2 R_2 R2中所有样本点标记的平均值。这里我们需要遍历所有属性及其属性对应的属性值。
②用选定的最优切分属性 j j j和最优切分值 s s s划分区域并以区域中相应样本的平均值作为输出值:
R 1 ( j , s ) = { x ∣ x ( j ) ≤ s } , R 2 ( j , s ) = { x ∣ x ( j ) ≥ s } R_1(j,s)={x|x^{(j)}≤s},R_2(j,s)={x|x^{(j)}≥s} R1(j,s)=xx(j)s,R2(j,s)=xx(j)s
c m ′ = 1 N m ∑ x i ∈ R m ( j , s ) y i , x ∈ R m , m = 1 , 2 c_m^{'}=\frac{1}{N_m}\sum_{x_i∈R_m(j,s)}y_i,x∈R_m,m=1,2 cm=Nm1xiRm(j,s)yi,xRm,m=1,2
③继续对两个子区域调用步骤①、②,直至满足停止条件
④将输入空间划分为M个区域 R 1 , R 2 , . . . , R M R_1,R_2,...,R_M R1,R2,...,RM,生成决策树:
f ( x ) = ∑ m = 1 M c m ′ I ( x ∈ R m ) f(x)=\sum_{m=1}^Mc_m^{'}I(x∈R_m) f(x)=m=1McmI(xRm)
这里我们所说的停止条件主要包括:①对于当前叶节点,如果再次切分的误差与不切分的误差相差不大,则可以停止切分;②对于当前叶节点,如果含有训练样本数量已经够少了,那么停止切分。

下面我们谈谈它与决策树的异同点:
①决策树只能用于分类问题,而CART树即可以用于回归问题又可以用于分类问题;
②在切分时,决策树的一个属性只能用一次,而CART树的一个属性可以多次用于切分;
③切分属性及属性取值的选择准则不同:决策树选择信息增益最大的特征及取值;对于CART树,分类问题选择基尼系数最小的特征及取值,回归问题选择使用是平方误差最小的特征及取值;
④CART数只能是一颗二叉树,而决策树根据属性的取值个数而定。

而对于,剪枝我们采用和决策树剪枝相同的方法:如果切分出的两个叶节点的平方误差并没有比两个叶节点合并的平方误差小,则将两个叶节点剪枝,合并到他们的父节点。

2、代码部分
首先,我们编写一个用于数据集切分的程序,我们输入数据集X,用于切分的属性axis以及切分属性的取值value

import numpy as np

def splitData(X,axis,value):
    leftX=X[np.nonzero(X[:,axis] <= value)[0]]
    rightX=X[np.nonzero(X[:,axis] > value)[0]]
    return leftX,rightX

这里leftX为切分属性axis小于等于切分值value的数据集,right为切分属性axis大于切分值value的数据集。
接着,我们编写对于一个数据集X的平方误差的函数sqrError和计算样本集X中所有样本的标记的平均:

def sqrError(X):
    sqrX = (X[:,-1] - X[:,-1].mean())**2
    error = sqrX.sum()
    return error

def Average(X):
    average = X[:,-1].mean()
    return average

下面为回归树的主函数:

def creatTree(X,errorType=sqrError,leafType=Average,ops=(1,4)):
    RegTree={}
    BestFeature,BestValue = chooseBestFeature(X,errorType,leafType,ops)
    if BestFeature == None:
        return BestValue
    leftX,rightX = splitData(X,BestFeature,BestValue)
    RegTree['splitFeature']=BestFeature
    RegTree['splitValue'] = BestValue
    RegTree['left'] = creatTree(leftX,errorType,leafType,ops)
    RegTree['right'] = creatTree(rightX,errorType,leafType,ops)
    return RegTree

这里输入X代表训练集;errorType表示我们训练过程中计算误差的方式,这里我们默认为sqrError,即平方误差;leafType代表我们计算叶节点取值的方式,这里默认参数为Average,即平方误差;ops代表需要输入的额外参数,下面我们会解释。
这里我们用字典去储存数的数据。我们用函数chooseBestFeature得到最佳的划分属性BestFeature以及最佳的划分值BestValue。有了最佳划分属性BestFeature以及最佳划分值BestValue,我们就可以得到切分后的两个切分后的数据集leftXrightX。然后我们用字典记录下最佳切分属性BestFeature和最佳切分值BestValue。由于当前最佳切分属性和最佳切分值对应的左子树和右子树存储切分的数据集leftXrightX。因为当前节点对应的左子树和右子树依然是一棵树,所以我们可以递归的调用creatTree去分别处理leftXrightX。递归结束条件为BestFeature == None,这里表示无需再切分,下面我们会在函数在chooseBestFeature解释。
下面为函数chooseBestFeature,即选择最佳属性的函数:

def chooseBestFeature(X,errorType,leafType,ops=(1,4)):
    deltaS=ops[0]
    minM=ops[1]
    m,d = X.shape
    d = d-1
    S = sqrError(X)
    bestError=100000
    if len(set(list(X[:,-1])))==1:
        return None,X[0,-1]
    for i in range(d):
        for splitValue in X[:,i]:
            leftX,rightX = splitData(X,i,splitValue)
            if (leftX.shape[0]<minM) or (rightX.shape[0]<minM):
                continue
            error=sqrError(leftX)+sqrError(rightX)
            if error<bestError:
                bestError=error
                bestFeature = i
                bestValue=splitValue
    if (S-bestError)<deltaS:
        return None,leafType(X)
    leftX,rightX = splitData(X,bestFeature,bestValue)
    return bestFeature,bestValue

这里的X为训练集;errorType表示计算误差的方法,这里使用的是平方误差;leafType表示我们对叶节点计算的方式,这里使用对叶节点包含的所有样本求平均;这里ops存放了两个参数,第一个参数deltaS表示如果我们从一个节点切分后的误差比不切分的误差小于这个参数deltaS,我们停止切分;第二个参数minM表示如果一个节点包含的样本数小于这个参数,那么我们停止切分。
这里我们遍历所有的特征和此属性对应的所有取值,找到误差最小的属性和取值。如果在遍历过程中发现此属性和取值分隔的样本集中小于我们设定的值minM,那么跳过这个这个属性和取值,寻找下一对属性和取值。遍历结束后,得到切分后的误差和不分割的误差,如果相差不大,则最佳属性bestFeature返回None,同时BestFeature == None也是我们主函数creatTree的递归结束条件,这时主程序也会停止递归;否则,正常返回最佳切分属性和取值bestFeaturebestValue
这里需要说明的是,ops中的两个参数的选定对于模型的泛化能力极为重要。如果deltaS过小,说明过于追求样本标记和预测值的拟合程度,会发生过拟合;如果minM过小,则该树容易切分过于细致,则也会发生过拟合。
下面为剪枝函数:

def decide_Tree(node):
    return ((type(node).__name__) == dict)

def get_mean(tree):
    if decide_Tree(tree['left']):
        tree['left'] = get_mean(tree['left'])
    if decide_Tree(tree['right']):
        tree['tight'] = get_mean(tree['right'])
    return ( tree['left']+tree['right'] ) / 2

def prune(tree,Xtest):
    if Xtest.shape[0]==0:
        return get_mean(tree)
    if decide_Tree(tree['left']) or decide_Tree(tree['right']):
        splitFeat = tree['splitFeature']
        splitValue = tree['splitValue']
        leftXtest,rightXtest = splitData(Xtest,splitFeat,splitValue)
    if decide_Tree(tree['left']):
        tree['left'] = prune(tree['left'],leftXtest)
    if decide_Tree(tree['right']):
        tree['right'] = prune(tree['right'],rightXtest)
    if (not decide_Tree(tree['left'])) and (not decide_Tree(tree['right'])):
        splitFeat = tree['splitFeature']
        splitValue = tree['splitValue']
        leftXtest,rightXtest = splitData(Xtest,splitFeat,splitValue)
        leftError = np.sum((leftXtest[:,-1] - tree['left'])**2)
        rightError = np.sum((rightXtest[:,-1] - tree['left'])**2)
        mergeNoError = leftError + rightError
        meanValue = (tree['left'] + tree['right']) / 2
        mergeError = np.sum((Xtest[:,-1] - meanValue)**2)
        if mergeNoError <= mergeError:
            return meanValue
        else:
            return tree
    else:
        return tree

首先,decide_Tree函数判断一个节点是否为数结构,如果是返回True。函数get_mean返回给节点的平均值,这里同样用了递归,如果输入的树的左右两个子节点为树,那么递归的对左右两个子节点继续求平均,直至左右两个子节点为叶节点时,对两个叶节点求平均。

如果当前节点测试时测试集只有一个样本,那么将当前节点对应的所有训练样本的平均作为该节点标记;第二个if语句用于判断当前测试节点的左右两个节点是否为树,如果是树,那么将依据该节点的最佳切分点和最佳切分值,对测试集进行划分;如果二个if语句执行,那么必执行第三、四个if语句,或两个都执行,或两个执行一个,这两个if主要用于判断该节点左右两个节点哪个节点为树,如果是树,那么进入子节点,继续进行剪枝;最后一个if,用于处理节点为单层树的情况,即该节点的子节点为两个叶节点,如果该节点为单层树,则计算该单层树两叶节点中样本的误差mergeError以及两叶节点剪枝后的误差mergeNoError。如果mergeNoError小于等于mergeError,说明划分效果在测试集中并没有那么好,那么我们将该节点对应的单层树直接改成叶节点,该节点对应的值为原来左右叶节点对应的值的平均值;否则,不做改变,返回原树。这里需要注意的是其实剪枝后的节点对应的值我们用左右叶节点对应的值的平均值来计算,但是这样计算并不完全准确,因为在训练过程中无法保证两叶节点中对应的样本数量相同,但是这样做却可以让树的整体误差下降。
这里同样需要说明:这个剪枝算法为贪心算法,因为剪枝为从下而上的,它只能考虑一些树的情况,并不能考虑所有情况。
下面我们检查一下这个算法的运行情况:
先检查数据情况:

print(open('F:/MachineLearning/data/ex0.txt').read())

得到:

1.000000        0.409175        1.883180
1.000000        0.182603        0.063908
1.000000        0.663687        3.042257
1.000000        0.517395        2.305004
1.000000        0.013643        -0.067698
1.000000        0.469643        1.662809
1.000000        0.725426        3.275749
1.000000        0.394350        1.118077
1.000000        0.507760        2.095059
1.000000        0.237395        1.181912
1.000000        0.057534        0.221663
1.000000        0.369820        0.938453
1.000000        0.976819        4.149409
1.000000        0.616051        3.105444
1.000000        0.413700        1.896278
1.000000        0.105279        -0.121345
1.000000        0.670273        3.161652
1.000000        0.952758        4.135358
1.000000        0.272316        0.859063
1.000000        0.303697        1.170272
1.000000        0.486698        1.687960
1.000000        0.511810        1.979745
1.000000        0.195865        0.068690
1.000000        0.986769        4.052137
1.000000        0.785623        3.156316
1.000000        0.797583        2.950630
1.000000        0.081306        0.068935
1.000000        0.659753        2.854020
1.000000        0.375270        0.999743
1.000000        0.819136        4.048082
1.000000        0.142432        0.230923
1.000000        0.215112        0.816693
1.000000        0.041270        0.130713
1.000000        0.044136        -0.537706
1.000000        0.131337        -0.339109
1.000000        0.463444        2.124538
1.000000        0.671905        2.708292
...

下面为数据处理函数:

def prepared_data(path):
    file = open(path)
    data_list=file.readlines()
    dataSet=[]
    for data in data_list:
        data = data.strip()
        data = data.split()
        data = [float(elem) for elem in data]
        dataSet.append(data)
    return np.array(dataSet)

数据处理,并查看,由于数据第一列全为1,所以可以去除:

data1 = prepared_data('F:/MachineLearning/data/ex0.txt')
data1 = np.delete(data1,0,axis=1)
print(data1)

得到:

[[ 4.091750e-01  1.883180e+00]
 [ 1.826030e-01  6.390800e-02]
 [ 6.636870e-01  3.042257e+00]
 [ 5.173950e-01  2.305004e+00]
 [ 1.364300e-02 -6.769800e-02]
 [ 4.696430e-01  1.662809e+00]
 [ 7.254260e-01  3.275749e+00]
 [ 3.943500e-01  1.118077e+00]
 [ 5.077600e-01  2.095059e+00]
 [ 2.373950e-01  1.181912e+00]
 [ 5.753400e-02  2.216630e-01]
 [ 3.698200e-01  9.384530e-01]
 [ 9.768190e-01  4.149409e+00]
 [ 6.160510e-01  3.105444e+00]
 [ 4.137000e-01  1.896278e+00]
 [ 1.052790e-01 -1.213450e-01]
 ...
 [ 1.200300e-02 -2.172830e-01]
 [ 1.888300e-02 -3.005770e-01]
 [ 7.147600e-02  6.014000e-03]]

我们在用图的形式查看,数据:

fig,ax = plt.subplots(figsize=(9,6))
ax.scatter(data1[:,0],data1[:,1])
plt.show()

得到:
在这里插入图片描述
这里我们可以看到明显可以数据可分为5类。
我们在用树回归函数,查看:

print(creatTree(data1))

得到:

{'splitFeature': 0, 'splitValue': 0.39435, 'left': {'splitFeature': 0, 'splitValue': 0.197834,
 'left': -0.023838155555555553, 'right': 1.0289583666666666},
  'right': {'splitFeature': 0, 'splitValue': 0.582002, 'left': 1.980035071428571,
   'right': {'splitFeature': 0, 'splitValue': 0.797583, 'left': 2.9836209534883724, 'right': 3.9871632}}}

我们通过这个树,可以清楚看到叶节点的五个值:-0.0238381555555555531.02895836666666661.9800350714285712.98362095348837243.9871632。这些值大致符合图中情况。

二、模型树
模型树的步骤大致和回归树相同,但是在切分完成之后,在对叶节点中的数据进行线性拟合,即对叶节点中的数据进行线性回归,比如如下数据图:
在这里插入图片描述
明显此数据可分为两段,但是两段数据又各有线性结构,我们只要给出每一段线性拟合的参数即可,叶节点的数据类型由代表样本平均值的数值变为保存参数的数组。而我们也只要对上面的回归树函数的参数errorTypeleafType改变即可,errorType改为线性回归的平方误差,leafType改为叶节点的拟合参数。errorTypeleafType对应的函数如下:
首先是每个节点线性拟合参数的函数:

def  linerFit(DataSet):
	X = DataSet[:,:-1]
	y = DataSet[:,-1]
	X_X = X.T @ X
	if np.linalg.det(X_X)==0:
		raise NameError('This mat can not inverse')
	theta = np.linalg.inv(X_X) @ X.T @ y
	return  theta,X,y

接着是作为leafType的函数:

def modelLeaf(dataSet):
	theta,X,y = linerFit(dataSet)
	return theta

作为errorType的误差计算函数:

def modelError(dataSet):
	theta,X,y = linerFit(dataSet)
	error = (X @ theta-  y).T @ (X @ theta - y)
	return error 

我们只要把回归树函数creatTree中的参数errorType设置为modelErrorleafType设置为modelLeaf即可。运行后,与回归树得到的结果进行比较,发现只是叶节点对应数据由数值变为储存参数数组,其他几乎无变化。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值