机器学习实战 学习记录 (8-9章)

这篇博客详细介绍了回归分析中的线性回归、局部加权线性回归以及CART算法。线性回归包括普通线性回归和岭回归,其中岭回归通过引入缩减系数来处理特征过多或矩阵非满秩的情况。局部加权线性回归则考虑了数据的局部性,通过高斯核赋予附近点更大权重。CART算法构建决策树,可用于处理非线性数据,同时讨论了预剪枝和后剪枝两种剪枝策略以防止过拟合。
摘要由CSDN通过智能技术生成

参考:机器学习实战Peter Harrington

(11条消息) 机器学习实战教程(13篇)_chenyanlong_v的博客-CSDN博客_机器学习实战

八、预测数值型数据:回归

优点:结果易于理解,计算简单

缺点:对非线性数据拟合效果不好

适用数据类型:数值型和标称型数据 

(1)假定输入数据存放在矩阵X中,而回归函数放在向量w中。那么对于给定的数据X1,预测结果将会通过Y1=X1^T * w给出。一个常用的方法是找出误差最小的w(这里的误差是指预测Y值和真实Y值之间的差值)。采用平方误差(平方误差和越小,说明线性回归拟合效果越好)可以写作:

用矩阵表示还可以写作:

用矩阵表示的平方误差和对w进行求导得到:

令上述公式等于0,得到:

w上方的小标记表示,这是当前可以估计出的w的最优解。从现有数据上估计出的w可能并不是数据中的真实w值,所以这里使用了一个"帽"符号来表示它仅是w的一个最佳估计。

示例1:

# -*- coding:utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def standRegres(xArr,yArr): #计算回归系数
    xMat = np.mat(xArr)
    yMat = np.mat(yArr).T
    xTx = xMat.T * xMat #根据文中推导的公式计算回归系数
    if np.linalg.det(xTx) == 0.0:
        print("矩阵为奇异矩阵,不能求逆")
        return
    ws = xTx.I * (xMat.T * yMat)
    return ws

def plotRegression(): #绘制回归曲线和数据点
    xArr,yArr = loadDataSet('ex0.txt') #加载数据集
    ws = standRegres(xArr,yArr) #计算回归系数
    xMat = np.mat(xArr) #创建xMat矩阵
    yMat = np.mat(yArr) #创建yMat矩阵
    xCopy = xMat.copy() #深拷贝xMat矩阵
    xCopy.sort(0) #排序
    yHat = xCopy * ws #计算对应的y值
    fig = plt.figure()
    ax = fig.add_subplot(111) #添加subplot
    ax.plot(xCopy[:,1],yHat,c = 'red') #绘制回归曲线
    ax.scatter(xMat[:,1].flatten().A[0],yMat.flatten().A[0],s = 20,c = 'blue',alpha = .5)
    plt.title('DataSet')
    plt.xlabel('X')
    plt.show()

if __name__ == '__main__':
    plotRegression()

如何判断拟合曲线的拟合效果的如何呢?,我们可以使用corrcoef方法,来比较预测值和真实值的相关性。 

# -*- coding:utf-8 -*-
import numpy as np

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def standRegres(xArr,yArr): #计算回归系数
    xMat = np.mat(xArr)
    yMat = np.mat(yArr).T
    xTx = xMat.T * xMat #根据文中推导的公式计算回归系数
    if np.linalg.det(xTx) == 0.0:
        print("矩阵为奇异矩阵,不能求逆")
        return
    ws = xTx.I * (xMat.T * yMat)
    return ws

if __name__ == '__main__':
    xArr,yArr = loadDataSet('ex0.txt')
    ws = standRegres(xArr,yArr)
    xMat = np.mat(xArr)
    yMat = np.mat(yArr)
    yHat = xMat * ws
    print(np.corrcoef(yHat.T,yMat))

可以看到,对角线上的数据是1.0,因为yMat和自己的匹配是完美的,而YHat和yMat的相关系数为0.98。

示例2:局部加权线性回归(LWLR)

线性回归的一个问题是有可能出现欠拟合现象,因为它求的是具有小均方误差的无偏估 计。显而易见,如果模型欠拟合将不能取得好的预测效果。所以有些方法允许在估计中引入一 些偏差,从而降低预测的均方误差。

其中的一个方法是局部加权线性回归,在该方法中,我们给待预测点附近的每个点赋予一定的权重。该算法解出回归系数W的形式如下: 

其中W是一个矩阵,用来给每个数据点赋予权重。

 LWLR使用"核"(与支持向量机中的核类似)来对附近的点赋予更高的权重。核的类型可以自由选择,最常用的核就是高斯核,高斯核对应的权重如下:

# -*- coding:utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def plotwlrRegression(): #绘制多条局部加权回归曲线
    xArr,yArr = loadDataSet('ex0.txt')
    yHat_1 = lwlrTest(xArr,xArr,yArr,1.0) ##根据局部加权线性回归计算yHat
    yHat_2 = lwlrTest(xArr,xArr,yArr,0.01)
    yHat_3 = lwlrTest(xArr,xArr,yArr,0.003)
    xMat = np.mat(xArr) #创建xMat矩阵
    yMat = np.mat(yArr) #创建yMat矩阵
    srtInd = xMat[:,1].argsort(0) #排序,返回索引值
    xSort = xMat[srtInd][:,0,:]
    fig,axs = plt.subplots(nrows = 3,ncols = 1,sharex = False,sharey = False,figsize = (10,8))
    axs[0].plot(xSort[:,1],yHat_1[srtInd],c = 'red') #绘制回归曲线
    axs[1].plot(xSort[:,1],yHat_2[srtInd],c = 'red')
    axs[2].plot(xSort[:,1],yHat_3[srtInd],c = 'red')
    axs[0].scatter(xMat[:,1].flatten().A[0],yMat.flatten().A[0],s = 20,c = 'blue',alpha = .5)
    axs[1].scatter(xMat[:,1].flatten().A[0],yMat.flatten().A[0],s = 20,c = 'blue',alpha = .5)
    axs[2].scatter(xMat[:,1].flatten().A[0],yMat.flatten().A[0],s = 20,c = 'blue',alpha = .5)
    axs0_title_text = axs[0].set_title('k=1.0') #设置标题
    axs1_title_text = axs[1].set_title('k=0.1')
    axs2_title_text = axs[2].set_title('k=0.003')
    plt.setp(axs0_title_text,size = 8,weight = 'bold',color = 'red')
    plt.setp(axs1_title_text,size = 8,weight = 'bold',color = 'red')
    plt.setp(axs2_title_text,size = 8,weight = 'bold',color = 'red')
    plt.xlabel('X')
    plt.show()

def lwlr(testPoint,xArr,yArr,k = 1.0): #使用局部加权线性回归计算回归系数w
    xMat = np.mat(xArr);yMat = np.mat(yArr).T
    m = np.shape(xMat)[0]
    weights = np.mat(np.eye((m))) #创建权重对角矩阵
    for j in range(m): #遍历数据集计算每个样本的权重
        diffMat = testPoint - xMat[j,:]
        weights[j,j] = np.exp(diffMat * diffMat.T/(-2.0 * k ** 2))
    xTx = xMat.T * (weights * xMat)
    if np.linalg.det(xTx) == 0.0:
        print("矩阵为奇异矩阵,不能求逆")
        return
    ws = xTx.I * (xMat.T * (weights * yMat)) #计算回归系数
    return testPoint * ws

def lwlrTest(testArr,xArr,yArr,k=1.0): #局部加权线性测试函数
    m = np.shape(testArr)[0] #计算测试数据集大小
    yHat = np.zeros(m)
    for i in range(m): #对每个样本点进行预测
        yHat[i] = lwlr(testArr[i],xArr,yArr,k)
    return yHat

if __name__ == '__main__':
    plotwlrRegression()

可以看到,当k越小,拟合效果越好。但是当k过小,会出现过拟合的情况,例如k等于0.003的时候。 

示例4:预测鲍鱼的年龄

# -*- coding:utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def lwlr(testPoint,xArr,yArr,k = 1.0): #使用局部加权线性回归计算回归系数w
    xMat = np.mat(xArr);yMat = np.mat(yArr).T
    m = np.shape(xMat)[0]
    weights = np.mat(np.eye((m))) #创建权重对角矩阵
    for j in range(m): #遍历数据集计算每个样本的权重
        diffMat = testPoint - xMat[j,:]
        weights[j,j] = np.exp(diffMat * diffMat.T/(-2.0 * k ** 2))
    xTx = xMat.T * (weights * xMat)
    if np.linalg.det(xTx) == 0.0:
        print("矩阵为奇异矩阵,不能求逆")
        return
    ws = xTx.I * (xMat.T * (weights * yMat)) #计算回归系数
    return testPoint * ws

def lwlrTest(testArr,xArr,yArr,k=1.0): #局部加权线性测试函数
    m = np.shape(testArr)[0] #计算测试数据集大小
    yHat = np.zeros(m)
    for i in range(m): #对每个样本点进行预测
        yHat[i] = lwlr(testArr[i],xArr,yArr,k)
    return yHat

def standRegres(xArr,yArr): #计算回归系数w
    xMat = np.mat(xArr);yMat = np.mat(yArr).T
    xTx = xMat.T * xMat
    if np.linalg.det(xTx) == 0.0:
        print("矩阵为奇异矩阵,不能求逆")
        return
    ws = xTx.I * (xMat.T * yMat)
    return ws

def rssError(yArr,yHatArr): #误差大小评价函数
    return ((yArr - yHatArr) **2).sum()

if __name__ == '__main__':
    abX,abY = loadDataSet('abalone.txt')
    print('训练集与测试集相同:局部加权线性回归,核k的大小对预测的影响:')
    yHat01 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],0.1)
    yHat1 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],1)
    yHat10 = lwlrTest(abX[0:99],abX[0:99],abY[0:99],10)
    print('k = 0.1时,误差大小为:',rssError(abY[0:99],yHat01.T))
    print('k = 1时,误差大小为:',rssError(abY[0:99],yHat1.T))
    print('k = 10时,误差大小为:',rssError(abY[0:99],yHat10.T))
    print('')
    print('训练集与测试集不同:局部加权线性回归,核k的大小是越小越好吗?更换数据集,测试结果如下:')
    yHat01 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],0.1)
    yHat1 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],1)
    yHat10 = lwlrTest(abX[100:199],abX[0:99],abY[0:99],10)
    print('k = 0.1时,误差大小为:', rssError(abY[100:199], yHat01.T))
    print('k = 1时,误差大小为:', rssError(abY[100:199], yHat1.T))
    print('k = 10时,误差大小为:', rssError(abY[100:199], yHat10.T))
    print('')
    print('训练集与测试集不同:简单的线性回归与k=1时的局部加权线性回归对比:')
    print('k = 1,误差大小为:',rssError(abY[100:199],yHat1.T))
    ws = standRegres(abX[0:99],abY[0:99])
    yHat = np.mat(abX[100:199]) * ws
    print('简单的线性回归误差大小:',rssError(abY[100:199],yHat.T.A))

当k=0.1时,训练集误差小,但是应用于新的数据集之后,误差反而变大了。这就是经常说道的过拟合现象。

核大小等于10时的测试误差最小,但它在训练集上的误差却是最大的。 

(2)缩减系数

①岭回归

如果数据的特征比样本点还多应该怎么办?很显然,此时我们不能再使用上文的方法进行计算了,因为矩阵X不是满秩矩阵,非满秩矩阵在求逆时会出现问题。为了解决这个问题,统计学家引入岭回归的概念。

岭回归即是在矩阵X^T X上加一个λI从而使矩阵非奇异,进而能对X^T X+λI求逆。其中矩阵I是一个m*m的单位矩阵(对角线上元素全为1,其他元素全为0),λ是一个用户定义的数值。计算公式变形如下:

岭回归最先用来处理特征数多于样本数的情况,现在也用于在估计中加入偏差,从而得到更好的估计。这里通过引入λ来限制了所有w之和,通过引入该惩罚项,能够减少不重要的参数,也称缩减。

与前几章训练其他参数所用的方法相似,这里通过预测误差最小化得到λ:数据获取之后,首先抽取一部分数据用于测试,剩余的作为训练集用于训练参数w。训练完毕后在测试集上测试预测性能,选取不同的λ来重复上述测试过程,最终得到一个使预测误差最小的

示例5:

为了使用岭回归和缩减技术,首先需要对特征做标准化处理。因为,我们需要使每个维度特征具有相同的重要性。本文使用的标准化处理比较简单,就是将所有特征都减去各自的均值并除以方差。

from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import numpy as np

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def ridgeRegres(xMat,yMat,lam = 0.2): #岭回归,lam为缩减系数
    xTx = xMat.T * xMat
    denom = xTx + np.eye(np.shape(xMat)[1]) * lam #eye生成单位矩阵
    if np.linalg.det(denom) == 0.0:
        print("矩阵为奇异矩阵,不能转置")
        return
    ws = denom.I * (xMat.T * yMat) #根据公式求w
    return ws

def ridgeTest(xArr,yArr): #岭回归测试
    xMat = np.mat(xArr); yMat = np.mat(yArr).T
    #数据标准化
    yMean = np.mean(yMat,axis = 0) #行与行操作,求均值
    yMat = yMat - yMean #数据减去均值
    xMeans = np.mean(xMat,axis = 0) #行与行操作,求均值
    xVar = np.var(xMat,axis = 0) #行与行操作,求方差
    xMat = (xMat - xMeans)/xVar #数据减去均值除以方差实现标准化
    numTestPts = 30 #30个不同的lambda测试
    wMat = np.zeros((numTestPts,np.shape(xMat)[1])) #初始回归系数矩阵
    for i in range(numTestPts): #改变lambda计算回归系数
        ws = ridgeRegres(xMat,yMat,np.exp(i-10)) #lambda以e的指数变化,最初是一个非常小的数
        wMat[i,:] = ws.T #计算回归系数矩阵
    return wMat

def plotwMat(): #绘制岭回归系数矩阵
    abX, abY = loadDataSet('abalone.txt')
    redgeWeights = ridgeTest(abX, abY)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(redgeWeights)
    ax_title_text = ax.set_title('The relationship between log (lambada) and the regression coefficient')
    ax_xlabel_text = ax.set_xlabel('log(lambada)')
    ax_ylabel_text = ax.set_ylabel('Regression coefficients')
    plt.setp(ax_title_text, size = 20, weight = 'bold', color = 'red')
    plt.setp(ax_xlabel_text, size = 10, weight = 'bold', color = 'black')
    plt.setp(ax_ylabel_text, size = 10, weight = 'bold', color = 'black')
    plt.show()

if __name__ == '__main__':
    plotwMat()

图绘制了回归系数与log(λ)的关系。在最左边,即λ最小时,可以得到所有系数的原始值(与线性回归一致);而在右边,系数全部缩减成0;在中间部分的某个位置,将会得到最好的预测结果。想要得到最佳的λ参数,可以使用交叉验证的方式获得。

②前向逐步回归

前向逐步线性回归算法属于一种贪心算法,即每一步都尽可能减少误差。我们计算回归系数,不再是通过公式计算,而是通过每次微调各个回归系数,然后计算预测误差。那个使误差最小的一组回归系数,就是我们需要的最佳回归系数。

示例6:

from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import numpy as np

def loadDataSet(fileName):
    numFeat = len(open(fileName).readline().split('\t')) - 1
    xArr = []
    yArr = []
    fr = open(fileName)
    for line in fr.readlines(): #逐行读取,滤除空格
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat):
            lineArr.append(float(curLine[i]))
        xArr.append(lineArr)
        yArr.append(float(curLine[-1]))
    return xArr,yArr

def regularize(xMat,yMat): #数据标准化
    inxMat = xMat.copy() #数据拷贝
    inyMat = yMat.copy()
    yMean = np.mean(yMat,0)
    inyMat = yMat - yMean
    inMeans = np.mean(inxMat,0)
    inVar = np.var(inxMat,0)
    inxMat = (inxMat - inMeans) / inVar
    return inxMat,inyMat

def rssError(yArr,yHatArr):
    return((yArr - yHatArr) ** 2).sum()

def stageWise(xArr, yArr, eps = 0.01, numIt = 100):
    xMat = np.mat(xArr); yMat = np.mat(yArr).T #数据集
    xMat, yMat = regularize(xMat, yMat) #数据标准化
    m, n = np.shape(xMat)
    returnMat = np.zeros((numIt, n)) #初始化numIt次迭代的回归系数矩阵
    ws = np.zeros((n, 1)) #初始化回归系数矩阵
    wsTest = ws.copy()
    wsMax = ws.copy()
    for i in range(numIt): #迭代numIt次
        print(ws.T) #打印当前回归系数矩阵
        lowestError = float('inf'); #正无穷
        for j in range(n): #遍历每个特征的回归系数
            for sign in [-1, 1]:
                wsTest = ws.copy()
                wsTest[j] += eps * sign #微调回归系数
                yTest = xMat * wsTest #计算预测值
                rssE = rssError(yMat.A, yTest.A) #计算平方误差
                if rssE < lowestError: #如果误差更小,则更新当前的最佳回归系数
                    lowestError = rssE
                    wsMax = wsTest
        ws = wsMax.copy()
        returnMat[i,:] = ws.T #记录numIt次迭代的回归系数矩阵
    return returnMat

def plotstageWiseMat():
    xArr, yArr = loadDataSet('abalone.txt')
    returnMat = stageWise(xArr, yArr, 0.005, 1000)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(returnMat)
    ax_title_text = ax.set_title('Forward stepwise regression: The number of iterations vs. the regression coefficient')
    ax_xlabel_text = ax.set_xlabel('The number of iterations')
    ax_ylabel_text = ax.set_ylabel('Regression coefficients')
    plt.setp(ax_title_text, size = 15, weight = 'bold', color = 'red')
    plt.setp(ax_xlabel_text, size = 10, weight = 'bold', color = 'black')
    plt.setp(ax_ylabel_text, size = 10, weight = 'bold', color = 'black')
    plt.show()

if __name__ == '__main__':
    plotstageWiseMat()

我们打印了迭代次数与回归系数的关系曲线。可以看到,有些系数从始至终都是约为0的,这说明它们不对目标造成任何影响,也就是说这些特征很可能是不需要的。逐步线性回归算法的优点在于它可以帮助人们理解有的模型并做出改进。当构建了一个模型后,可以运行该算法找出重要的特征,这样就有可能及时停止对那些不重要特征的收集。 

 当应用缩减方法时,模型就增加了偏差,与此同时减小了方差。

 

缩减方法(逐步线性回归或岭回归),就是将一些系数缩减成很小的值或者直接缩减为0。这样做,就增大了模型的偏差(减少了一些特征的权重),通过把一些特征的回归系数缩减到0,同时也就减少了模型的复杂度。消除了多余的特征之后,模型更容易理解,同时也降低了预测误差。但是当缩减过于严厉的时候,就会出现过拟合的现象,即用训练集预测结果很好,用测试集预测就糟糕很多。 

 示例7:预测乐高玩具套装的价格

九、树回归

第八章介绍的线性回归需要拟合所有的样本点(局部加权线性回归除外)。而且,很多实际问题都是非线性的,不可能使用全局线性模型来拟合任何数据。

一种可行的方法是将数据集切分成很多份易建模的数据,然后利用线性回归进行建模。在这种切分方式下,树结构和回归法就相当有用。

优点:可以对复杂和非线性的数据建模

缺点:结果不易理解

适用数据类型:数值型和标称型

(1)CART算法

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

除了切分过于迅速外,ID3算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征离散化,才能在ID3算法中使用。但这种转换过程会破坏连续型变量的内在特性。

与ID3算法相反,CART算法正好适用于连续型特征。CART算法使用二元切分法来处理连续型变量。而使用二元切分法则易于对树构建过程进行调整以处理连续型特征。具体的处理方法是:如果特征值大于给定值就走左子树,否则就走右子树。

→CART算法:①决策树生成:递归地构建二叉决策树的过程,基于训练数据集生成决策树,生成的决策树要尽量大;自上而下从根开始建立节点,在每个节点处要选择一个最好的属性来分裂,不同的算法使用不同的指标来定义"最好"。

②决策树剪枝:用验证数据集对已生成的树进行剪枝并选择最优子树,这时损失函数最小作为剪枝的标准。

在决策树的文章中,我们先根据信息熵的计算找到最佳特征切分数据集构建决策树。CART算法的决策树生成也是如此。

示例1:回归树的切分函数

#-*- coding:utf-8 -*-
import numpy as np

def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float,curLine)) #转化为float类型
        dataMat.append(fltLine)
    return dataMat

def binSpiltDataSet(dataSet,feature,value): #根据特征切分数据集合
    mat0 = dataSet[np.nonzero(dataSet[:,feature] > value)[0],:]
    mat1 = dataSet[np.nonzero(dataSet[:,feature] <= value)[0],:]
    return mat0,mat1

def regLeaf(dataSet): #生成叶节点
    return np.mean(dataSet[:,-1]) #返回该目标变量的均值

def regErr(dataSet): #误差估计函数
    return np.var(dataSet[:,-1]) * np.shape(dataSet)[0] #返回目标变量的总方差

def chooseBestSplit(dataSet,leafType = regLeaf,errType = regErr,ops = (1,4)): #找到数据的最佳二元切分方式函数
    tolS = ops[0];tolN = ops[1] #tolS允许的误差下降值,tolN切分的最少样本数
    if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果当前所有值相等,则退出。(根据set的特性)
        return None,leafType(dataSet)
    m,n = np.shape(dataSet)
    S = errType(dataSet) #默认最后一个特征为最佳切分特征,计算其误差估计
    bestS = float('inf');bestIndex = 0;bestValue = 0 #分别为最佳误差,最佳特征切分的索引值,最佳特征值
    for featIndex in range(n-1): #遍历所有特征列
        for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #遍历所有特征值
            mat0,mat1 = binSpiltDataSet(dataSet,featIndex,splitVal) #根据特征和特征值切分数据集
            if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):continue #如果数据少于tolN,则退出
            newS = errType(mat0) + errType(mat1) #计算误差估计
            if newS < bestS: #如果误差估计更小,则更新特征索引值和特征值
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    if (S - bestS) < tolS: #如果误差减少不大则退出
        return None,leafType(dataSet)
    mat0,mat1 = binSpiltDataSet(dataSet,bestIndex,bestValue) #根据最佳的切分特征和特征值切分数据集合
    if(np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): #如果切分出的数据集很小则退出
        return None,leafType(dataSet)
    return bestIndex,bestValue

if __name__ == '__main__':
    myDat = loadDataSet('ex00.txt')
    myMat = np.mat(myDat)
    feat,val = chooseBestSplit(myMat,regLeaf,regErr,(1,4))
    print(feat)
    print(val)

可以看到,切分的最佳特征为第1列特征,最佳切分特征值为0.48813。

切分的特征和特征值我们已经选择好了,接下来就是利用选出的这两个变量创建回归树了。

创建方法很简单,我们根据切分的特征和特征值切分出两个数据集,然后将两个数据集分别用于左子树的构建和右子树的构建,直到无法找到切分的特征为止。因此,我们可以使用递归实现这个过程。

#-*- coding:utf-8 -*-
import numpy as np

def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float,curLine)) #转化为float类型
        dataMat.append(fltLine)
    return dataMat

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

def regLeaf(dataSet): #生成叶节点
    return np.mean(dataSet[:,-1]) #返回该目标变量的均值

def regErr(dataSet): #误差估计函数
    return np.var(dataSet[:,-1]) * np.shape(dataSet)[0] #返回目标变量的总方差

def chooseBestSplit(dataSet,leafType = regLeaf,errType = regErr,ops = (1,4)): #找到数据的最佳二元切分方式函数
    tolS = ops[0];tolN = ops[1] #tolS允许的误差下降值,tolN切分的最少样本数
    if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果当前所有值相等,则退出。(根据set的特性)
        return None,leafType(dataSet)
    m,n = np.shape(dataSet)
    S = errType(dataSet) #默认最后一个特征为最佳切分特征,计算其误差估计
    bestS = float('inf');bestIndex = 0;bestValue = 0 #分别为最佳误差,最佳特征切分的索引值,最佳特征值
    for featIndex in range(n-1): #遍历所有特征列
        for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #遍历所有特征值
            mat0,mat1 = binSpiltDataSet(dataSet,featIndex,splitVal) #根据特征和特征值切分数据集
            if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):continue #如果数据少于tolN,则退出
            newS = errType(mat0) + errType(mat1) #计算误差估计
            if newS < bestS: #如果误差估计更小,则更新特征索引值和特征值
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    if (S - bestS) < tolS: #如果误差减少不大则退出
        return None,leafType(dataSet)
    mat0,mat1 = binSpiltDataSet(dataSet,bestIndex,bestValue) #根据最佳的切分特征和特征值切分数据集合
    if(np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): #如果切分出的数据集很小则退出
        return None,leafType(dataSet)
    return bestIndex,bestValue

def createTree(dataSet,leafType = regLeaf,errType = regErr,ops=(1,4)): #树构建函数
    feat,val = chooseBestSplit(dataSet,leafType,errType,ops) #选择最佳切分特征和特征值
    if feat == None:return val #r如果没有特征,则返回特征值,提前终止
    retTree = {} #回归树
    retTree['spInd'] = feat
    retTree['spVal'] = val
    lSet,rSet = binSpiltDataSet(dataSet,feat,val) #分成左数据集和右数据集
    retTree['left'] = createTree(lSet,leafType,errType,ops) #创建左子树和右子树
    retTree['right'] = createTree(rSet,leafType,errType,ops)
    return retTree

if __name__ == '__main__':
    myDat = loadDataSet('ex00.txt')
    myMat = np.mat(myDat)
    print(createTree(myMat))

从上图可知,这棵树只有两个叶结点。

 (2)树剪枝

一棵树如果节点过多,表明该模型可能对数据进行了“过拟合”,通过降低树的复杂度来避免过拟合的过程称为剪枝。其实前面也进行过剪枝处理,在函数chooseBestSplit()中的提前终止条件,实际上是在进行一种所谓的预剪枝操作。

预剪枝有一定的局限性,比如我们现在使用一个新的数据集。

可以看到,对于这个数据集与我们使用的第一个数据集很相似,但是区别在于y的数量级差100倍,数据分布相似,因此构建出的树应该也是只有两个叶结点。但是我们使用默认tolS和tolN参数创建树,你会发现运行结果如下所示:


构建出的树有很多叶结点。产生这个现象的原因在于,停止条件tolS对误差的数量级十分敏感。 如果在选项中花费时间并对上述误差容忍度取平均值,或许也能得到仅有两个叶结点组成的树:

if __name__ == '__main__':
    myDat = loadDataSet('ex2.txt')
    myMat = np.mat(myDat)
    print(createTree(myMat,ops = (10000,4)))

将参数tolS修改为10000后,构建的树就是只有两个叶结点。显然这个值,需要我们经过不断测试得来,而通过不断修改停止条件来得到合理结果并不是很好的办法,我们常常甚至不确定到底需要寻找什么样的结果。因为对于一个很多维度的数据集,你也不知道构建的树需要多少个叶结点。

可见,预剪枝有很大的局限性。接下来,我们讨论后剪枝,即利用测试集来对树进行剪枝。由于不需要用户指定参数,后剪枝是一个更理想化的剪枝方法。

(3)后剪枝

使用后剪枝方法需要将数据集分成测试集和训练集。首先指定参数,使得构建出的树足够大、足够复杂,便于剪枝。接下来从上而下找到叶结点,用测试集来判断这些叶结点合并是否能降低测试集误差。如果是的话就合并。

#-*- coding:utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np


def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float,curLine)) #转化为float类型
        dataMat.append(fltLine)
    return dataMat

def plotData(filename):
    dataMat = loadDataSet(filename)
    n = len(dataMat)
    xcord = [];ycord = []
    for i in range(n):
        xcord.append(dataMat[i][0]);ycord.append(dataMat[i][1])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord,ycord,s = 20,c = 'blue',alpha = .5) #绘制样本点
    plt.title('DataSet')
    plt.xlabel('X')
    plt.show()

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

def regLeaf(dataSet): #生成叶节点
    return np.mean(dataSet[:,-1]) #返回该目标变量的均值

def regErr(dataSet): #误差估计函数
    return np.var(dataSet[:,-1]) * np.shape(dataSet)[0] #返回目标变量的总方差

def chooseBestSplit(dataSet,leafType = regLeaf,errType = regErr,ops = (1,4)): #找到数据的最佳二元切分方式函数
    tolS = ops[0];tolN = ops[1] #tolS允许的误差下降值,tolN切分的最少样本数
    if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #如果当前所有值相等,则退出。(根据set的特性)
        return None,leafType(dataSet)
    m,n = np.shape(dataSet)
    S = errType(dataSet) #默认最后一个特征为最佳切分特征,计算其误差估计
    bestS = float('inf');bestIndex = 0;bestValue = 0 #分别为最佳误差,最佳特征切分的索引值,最佳特征值
    for featIndex in range(n-1): #遍历所有特征列
        for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]): #遍历所有特征值
            mat0,mat1 = binSpiltDataSet(dataSet,featIndex,splitVal) #根据特征和特征值切分数据集
            if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):continue #如果数据少于tolN,则退出
            newS = errType(mat0) + errType(mat1) #计算误差估计
            if newS < bestS: #如果误差估计更小,则更新特征索引值和特征值
                bestIndex = featIndex
                bestValue = splitVal
                bestS = newS
    if (S - bestS) < tolS: #如果误差减少不大则退出
        return None,leafType(dataSet)
    mat0,mat1 = binSpiltDataSet(dataSet,bestIndex,bestValue) #根据最佳的切分特征和特征值切分数据集合
    if(np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): #如果切分出的数据集很小则退出
        return None,leafType(dataSet)
    return bestIndex,bestValue

def createTree(dataSet,leafType = regLeaf,errType = regErr,ops=(1,4)): #树构建函数
    feat,val = chooseBestSplit(dataSet,leafType,errType,ops) #选择最佳切分特征和特征值
    if feat == None:return val #r如果没有特征,则返回特征值
    retTree = {} #回归树
    retTree['spInd'] = feat
    retTree['spVal'] = val
    lSet,rSet = binSpiltDataSet(dataSet,feat,val) #分成左数据集和右数据集
    retTree['left'] = createTree(lSet,leafType,errType,ops) #创建左子树和右子树
    retTree['right'] = createTree(rSet,leafType,errType,ops)
    return retTree

def isTree(obj): #判断测试输入变量是否是一棵树,obj是测试对象
    import types
    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 np.shape(testData)[0] == 0: return getMean(tree) #如果测试集为空,则对树进行塌陷处理
    if (isTree(tree['right']) or isTree(tree['left'])): #如果有左子树或者右子树,则切分数据集
        lSet,rSet = binSpiltDataSet(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 = binSpiltDataSet(testData,tree['spInd'],tree['spVal'])
        errorNoMerge = sum(np.power(lSet[:,-1] - tree['left'],2)) + sum(np.power(rSet[:,-1] - tree['right'],2)) #计算没有合并的误差
        treeMean = (tree['left'] + tree['right']) / 2.0 #计算合并的均值
        errorMerge = sum(np.power(testData[:,-1] - treeMean,2)) #计算合并的误差
        if errorMerge < errorNoMerge: #如果合并的误差小于没有合并的误差,则合并
            print("merging")
            return treeMean
        else: return tree
    else: return tree

if __name__ == '__main__':
    print('剪枝前:')
    train_filename = 'ex2.txt'
    train_data = loadDataSet(train_filename)
    train_Mat = np.mat(train_data)
    tree = createTree(train_Mat)
    print(tree)
    print('剪枝后:')
    test_filename = 'ex2test.txt'
    test_Data = loadDataSet(test_filename)
    test_Mat = np.mat(test_Data)
    print(prune(tree,test_Mat))

树的大量结点已经被剪枝掉了,但没有像预期的那样剪枝成两部分,这说明后剪枝可能不如预剪枝有效。一般地,为了寻求最佳模型可以同时使用两种剪枝技术。 

(4)模型树

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值