《机器学习实战》学习笔记(九)

第9章 树回归

引言

上一章的线性回归包含了一些强大的方法,但这些方法创建的模型需要拟合所有的样本点(局部加权线性回归除外)。

当数据拥有众多特征并且特征之间关系十分复杂时,构建全局模型的想法就显得太难了,也略显笨拙。而且,实际生活中很多问题都是非线性的,不可能使用全局线性模型来拟合任何数据。

CART(Classification And Regression Trees, 分类回归树)该算法既可以用于分类还可以用于回归。

树回归的优缺点

  • 优点:可以对复杂和非线性的数据建模。
  • 缺点:结果不易理解。
  • 适用数据类型:数值型和标称型数据。

树回归的一般方法

  1. 收集数据: 采用任意方法收集数据。
  2. 准备数据:需要数值型的数据,标称型数据应该映射成二值型数据。
  3. 分析数据: 绘出数据的二维可视化显示结果,以字典方式生成树。
  4. 训练算法 :大部分时间都花费在叶节点树模型的构建上。
  5. 测试算法 :使用测试数据上的^ 值来分析模型的效果。
  6. 使用算法:使用训练出的树做预测,预测結果还可以用来做很多事情

9.1 复杂数据的局部性建模

第三章决策树使用的树构建算法是ID3。ID3的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有4种取值,那么数据将被切成4份。一旦按某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。除了切分过于迅速外,ID3算法还存在另一个问题,它不能直接处理连续型特征

另外一种方法是二元切分法,即每次把数据集切成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进人树的左子树,反之则进人树的右子树。

二元切分法则易于对树构建过程进行调整以处理连续型特征。处理方法是:特征值大于给定值就走左子树,否则就走右子树。

9.2 连续和离散型特征的树的构建

使用一部字典来存储树的数据结构,该字典将包含以下4个元素。

  • 待切分的特征。
  • 待切分的特征值。
  • 右子树。当不再需要切分的时候,也可以是单个值。
  • 左子树。与右字树类似。

创建树函数createTree()的伪代码大致如下:

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

实现代码如下:

#切分数组
def binSplitDataSet(dataSet, feature, value):
    #首先获得feature列中元素大于value所在的行索引数组和列索引数组,
    #然后取行索引数组,然后获取dataSet矩阵中行索引数组所对应的数据构成新矩阵,
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
    #首先获得feature列中元素小于等于value所在的行索引数组和列索引数组,
    #然后取行索引数组,然后获取dataSet矩阵中行索引数组所对应的数据构成新矩阵,
    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 
    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  
#创建一个对角矩郑
testMat = mat(eye (4))
mat0 , mat1 = binSplitDataSet(testMat, 1, 0.5)

print("mat0 = " ,end="\n")
print(mat0)
print("mat1 = " ,end="\n")
print(mat1)

缺少chooseBestSplit()函数,所以只展示了二元分割函数,输出结果如下:
在这里插入图片描述
可以看到,函数将对角矩阵第二行成功与对角矩阵分割开来。

9.3 将CART算法用于回归

回归树假设叶节点是常数值,这种策略认为数据中的复杂关系可以用树结构来概括。

为成功构建以分段常数为叶节点的树,需要度量出数据的一致性。

在数据集上计算混乱度,首先计算所有数据的均值,然后计算每条数据的值到均值的差值(一般使用绝对值或平方值)。

找到数据集切分的最佳位置函数chooseBestSplit(),该函数遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。
伪代码如下:

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

实现代码如下:

#建立叶节点的函数
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]
    # 特征剩余数量为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) #exit cond 2
    mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
    #样本数量小于设定值,则不切分
    if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):  #exit cond 3
        return None, leafType(dataSet)
    #分裂属性和对应的值
    return bestIndex,bestValue
 
myDat = loadDataSet(r"./Ch09/ex00.txt")
myMat = mat(myDat)
myTree = createTree(myMat)

print(myTree)

输出结果如下;
在这里插入图片描述
由输出结果可知,输出只包含两个叶子节点。
数据集ex0.txt输出结果如下:
在这里插入图片描述
输出包括五个叶子节点。
为了便于观察,将数据集打印出来。
打印函数如下 :

#打印数据集
def plotData(myDat):
    x_axis = [];y_axis=[]    

    for i in range(len(myDat)):
        x_axis.append(myDat[i][-2])
        y_axis.append(myDat[i][-1])

    plt.scatter(x_axis, y_axis)   
    plt.show()
    return

ex00.txt数据集打印出来如下:
在这里插入图片描述
上图可知数据集主要可以划分为两块,所以生成的树有两个叶子。
ex0.txt数据集有三个特征值,但第一个特征值全部相同所以去第二第三个特征值绘制散点图,ex0.txt 打印出来如下:
在这里插入图片描述
如图所示,大致可以分为五块,而程序划分得到的叶子节点也为5个。

9.4 树剪枝

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

预剪枝分析

树构建算法其实对输人的参数tolS和tolN非常敏感。

修改参数ops为(0,1)数据集ex00.txt输出结果如下:
在这里插入图片描述
可以看到树结构十分臃肿。
接下来对数据集ex2,txt进行构建树。数据集散点图如下:
在这里插入图片描述
该数据集散点图与ex00特别相似,只有唯一的差别前者的数量级是后者的100倍。对改数据使用ops= (0,4)进行构建树得到树如下:
在这里插入图片描述
可以发现树叶子节点多了很多。产生这个现象的原因在于,停止条件tols对误差的数量级十分敏感。

修改参数ops为(10000,4)构建树结果如下:
在这里插入图片描述
可以看到得到的树只有两个叶子节点。与数据集ex00得到的树叶子相同。

通过不断修改停止条件来得到合理结果并不是很好的办法。事实上,我们常常甚至不确定到底需要寻找什么样的结果。这正是机器学习所关注的内容,计算机应该可以给出总体的概貌。

后剪枝

函数prune()的伪代码如下:

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

#测试输人变量是否是一棵树,返回布尔类型的结果
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))
        #合并后的叶子 均值
        treeMean = (tree['left']+tree['right'])/2.0
        # 合并后 的误差
        errorMerge = sum(power(testData[:,-1] - treeMean,2))
         # 合并后的误差小于合并前的误差
        if errorMerge < errorNoMerge: 
            print ("merging"# 返回两个叶子 的均值 作为 合并后的叶子节点
            return treeMean
        else: return tree
    else: return tree
    

输出结果如下:
在这里插入图片描述
如图可知,大量的节点已经被剪枝掉了,但没有像预期的那样剪枝成两部分,这说明后剪枝可能不如预剪枝有效。

9.5 模型树

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

如下图数据集:
在这里插入图片描述

决策树相比于其他机器学习算法的优势之一在于结果更易理解。很显然,两条直线比很多节点组成一棵大树更容易解释。模型树的可解释性是它优于回归树的特点之一。另外,模型树也具有更髙的预测准确度。

模型树的叶节点生成函数

# 线性模型
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('行列式值为零,不能计算逆矩阵,可适当增加ops的第二个值')

    ws = xTx.I * (X.T * Y)
    return ws,X,Y

# 模型叶子节点
def modelLeaf(dataSet):#create linear model and return coeficients
    ws,X,Y = linearSolve(dataSet)
    return ws

# 计算模型误差
def modelErr(dataSet):
    ws,X,Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(power(Y - yHat,2))


myDat = loadDataSet(r"./Ch09/exp2.txt")
myMat = mat(myDat)
plotData(myDat)
myTree = createTree(myMat,modelLeaf,modelErr,ops=(1,10))
print(myTree)

输出结果如下:
在这里插入图片描述
生成的这两个线性模型分别是y=3.469 + 1.1852x和x=0.001 6985+11.96477x,

9.6 示例:树回归与标准回归的比较

比较客观的方法是计算相关系数,也称为 R 2 R^2 R2值 。该相关系数可以通过调用NumPy库中的命令corrcoef (yHat,y,rowvar)来求解,其中yHat是预测值,y是目标变量的实际值。

R 2 R^2 R2值越接近1.0越好。

用树回归进行预测的代码:

# 线性叶子节点 预测计算函数 直接返回 树叶子节点 值   
def regTreeEval(model, inDat):
    return float(model)

def modelTreeEval(model, inDat):
    n = shape(inDat)[1]
    # 增加一列
    X = mat(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)

# 得到预测值
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

训练数据集bikeSpeedVsIq_train如下:
在这里插入图片描述
测试数据集bikeSpeedVsIq_test如下:
在这里插入图片描述
比较代码实现如下:


TestMat = mat(loadDataSet(r'./Ch09/bikeSpeedVsIq_test.txt'))
plotData(loadDataSet(r'./Ch09/bikeSpeedVsIq_test.txt'))
TrainMat = mat(loadDataSet(r'./Ch09/bikeSpeedVsIq_train.txt'))
plotData(loadDataSet(r'./Ch09/bikeSpeedVsIq_train.txt'))
StaTree = createTree(TrainMat, ops=(1,20))
StaYHat = createForeCast(StaTree, TestMat[:,0], regTreeEval)# 第一列为 自变量
# 预测结果和真实标签的相关系数
# NumPy 库函数
StaCorr = corrcoef(StaYHat, TestMat[:,1], rowvar=0)[0,1] 
# 得到模型回归树
ModeTree = createTree(TrainMat,leafType=modelLeaf, errType=modelErr, ops=(1,20))
# 得到预测结果
ModeYHat = createForeCast(ModeTree, TestMat[:,0], modelTreeEval)
# 预测结果和真实标签的相关系数
ModeCorr = corrcoef(ModeYHat, TestMat[:,1], rowvar=0)[0,1] # NumPy 库函数
print ("普通回归树 预测结果的相关系数R2: %f" %(StaCorr))
print ("模型回归树 预测结果的相关系数R2: %f" %(ModeCorr))

if ModeCorr>StaCorr:
    print ("模型回归树效果优于普通回归树")
else:
    print ("回归回归树效果优于模型普通树")

输出结果如下:
在这里插入图片描述
通过以上输出结果比较可以得到,模型树的结果比回归树在相关系数的值上的表现要更好。
标准的线性回归效果

ws , X , Y = linearSolve (TrainMat)

for i in range(shape (TestMat ) [0] ) :
    StaYHat[i]=TestMat[i,0 ] * ws[1,0]+ ws[0,0]

linercor = corrcoef(StaYHat, TestMat[:,1], rowvar=0)[0,1]

print('linercor = %s'%str(linercor))

输出结果如下:

linercor = 0.9434684235674763

对比结果可以得到,树回归方法在预测复杂数据时会比简单的线性模型更有效。

小结

本章学习了CART算法并构建了二元树。学习了预剪枝(在树的构建过程中就进行剪枝)和后剪枝(当树构建完毕再进行剪枝)的相关概念与代码实现,最后比较了树回归与之前的线性回归。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值