機器學習實戰(八)02-樹迴歸基礎篇之樹模型

一、模型樹

在迴歸樹的基礎上,葉子節點是常數值,如果這些葉子結點設定爲分段線性函數,這裏的所謂的分段線性是指模型有多個線性片段組成。
該算法的關鍵在於誤差的計算:
怎麼找到最佳切分點,應該怎樣計算誤差,首先對於給定的數據集,應該先用線性的模型對它擬合,然後計算真實目標值和模型預測值的差值,最後將這些差值的平方和得到所需的誤差。
注:線性迴歸(最小二乘)
通過最小化平方誤差,求解迴歸係數w。即平方誤差對w求導=0,解w。

 

w=(X.T * X).I * X.T *Y 

代碼實現:

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


def loadDataSet(fileName):
    """
    函數說明:加載數據
    Parameters:
        fileName - 文件名
    Returns:
        dataMat - 數據矩陣
    """
    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 plotDataSet(filename):
    """
    函數說明:繪製數據集
    Parameters:
        filename - 文件名
    Returns:
        無
    """
    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)  # 添加subplot
    ax.scatter(xcord, ycord, s=20, c='blue', alpha=.5)  # 繪製樣本點
    plt.title('DataSet')  # 繪製title
    plt.xlabel('X')
    plt.show()


def binSplitDataSet(dataSet, feature, value):
    """
    函數說明:根據特徵切分數據集合
    Parameters:
        dataSet - 數據集合
        feature - 帶切分的特徵
        value - 該特徵的值
    Returns:
        mat0 - 切分的數據集合0
        mat1 - 切分的數據集合1
    """
    mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
    mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
    return mat0, mat1


def regLeaf(dataSet):
    """
    函數說明:生成葉結點
    Parameters:
        dataSet - 數據集合
    Returns:
        目標變量的均值
    """
    return np.mean(dataSet[:, -1])


def regErr(dataSet):
    """
    函數說明:誤差估計函數
    Parameters:
        dataSet - 數據集合
    Returns:
        目標變量的總方差
    """
    return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]


# 線性模型

def linearSolve(dataSet): 
    """
    將數據格式化爲目標變量Y和自變量X
    :param dataSet:
    :return:
    """
    m, n = np.shape(dataSet)
    X = np.mat(np.ones((m, n)));
    Y = np.mat(np.ones((m, 1)))  # create a copy of data with 1 in 0th postion
    X[:, 1:n] = dataSet[:, 0:n - 1];
    Y = dataSet[:, -1]  # and strip out Y
    xTx = X.T * X
    # 判斷是否爲奇異矩陣
    # 若等於0,稱矩陣A爲奇異矩陣 ;非奇異矩陣是可逆的。
    if np.linalg.det(xTx) == 0.0:
        raise NameError('行列式值爲零,不能計算逆矩陣,可適當增加ops的第二個值')
    ws = xTx.I * (X.T * Y)
    return ws, X, Y


def modelLeaf(dataSet):  # 當數據不在需要切分的時候,生成葉子節點模型
    ws, X, Y = linearSolve(dataSet)
    return ws


def modelErr(dataSet):  # 計算誤差
    ws, X, Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(np.power(Y - yHat, 2))


def chooseBestSplit(dataSet, leafType=modelLeaf, errType=modelErr, ops=(1, 10)):
    """
    函數說明:找到數據的最佳二元切分方式函數
    Parameters:
        dataSet - 數據集合
        leafType - 生成葉結點
        regErr - 誤差估計函數
        ops - 用戶定義的參數構成的元組
    Returns:
        bestIndex - 最佳切分特徵
        bestValue - 最佳特徵值
    """
    import types
    # tolS允許的誤差下降值,tolN切分的最少樣本數
    tolS = ops[0];
    tolN = ops[1]
    # 如果當前所有值相等,則退出。(根據set的特性)
    if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
        return None, leafType(dataSet)
    # 統計數據集合的行m和列n
    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 = binSplitDataSet(dataSet, featIndex, splitVal)
            # 如果數據少於tolN,則退出
            if (np.shape(mat0)[0] < tolN) or (np.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)
    # 如果切分出的數據集很小則退出
    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, 10)):
    """
    函數說明:樹構建函數
    Parameters:
        dataSet - 數據集合
        leafType - 建立葉結點的函數
        errType - 誤差計算函數
        ops - 包含樹構建所有其他參數的元組
    Returns:
        retTree - 構建的迴歸樹
    """
    # 選擇最佳切分特徵和特徵值
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
    # r如果沒有特徵,則返回特徵值
    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



if __name__ == '__main__':
    train_filename = './data/exp2.txt'
    plotDataSet(train_filename)
    train_Data = loadDataSet(train_filename)
    train_Mat = np.mat(train_Data)
    myTree = createTree(train_Mat,modelLeaf, modelErr)
    print(myTree)

結果顯示:

{'spInd': 0, 'spVal': 0.285477, 'left': matrix([[1.69855694e-03],
        [1.19647739e+01]]), 'right': matrix([[3.46877936],
        [1.18521743]])}

可以看出,改代碼以0.285477爲界創建了兩個模型,下圖數據實際在0.3處分段。createTree() 生成的兩個線性模型分別是 y = 3.468 + 1.1852 *x 和 y = 0.0016985+ 11.96477 *x 與用於生成該樹的真實模型非常相近。

這裏寫圖片描述

二、樹迴歸和標準迴歸的比較

迴歸樹每個節點(不一定是葉子節點)都會得一個預測值,以年齡爲例,該預測值等於屬於這個節點的所有人年齡的平均值。模型樹和迴歸樹不同的是葉子結點是一個線性模型(線性公式:y = a*x + b ,裏面有a, b兩個數值。)
怎麼判斷兩個模型那個好啊,使用一個函數corrcoef(ModeYHat, TestMat[:,1], rowvar=0)[0,1] # NumPy 庫函數;根據樹結構,把數據的特徵測試模型,跑出實際結果和原有標籤數據比較得到。
迴歸樹:先把構建好的樹帶入函數createForeCast,得到預測值(1.使用模型和每一個數據的特徵作匹配;1>>裏面涉及到構建模型的特徵的索引,進一步得到相對應測試數據的值,之後和構建模型的分割數值比較。2>>只說做分支:大於此值就分到左節點那邊,如果左節點還是一棵樹,遞歸直到找到葉子結點,遇到後,返回這個葉子結點的值。2.最終得到一列含有實際標籤值的矩陣。),和標籤比較哈。
模型樹:和迴歸樹差不多,這一個特徵的測試數據添加一個偏移量,組成一個[[ 1. 12.]] 乘以構建模型中的迴歸係數,返回,最終得到一列含有實際標籤值的矩陣,和標籤比較哈。


代碼實現:

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


def loadDataSet(fileName):
    """
    函數說明:加載數據
    Parameters:
        fileName - 文件名
    Returns:
        dataMat - 數據矩陣
    """
    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 plotDataSet(filename):
    """
    函數說明:繪製數據集
    Parameters:
        filename - 文件名
    Returns:
        無
    """
    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)  # 添加subplot
    ax.scatter(xcord, ycord, s=20, c='blue', alpha=.5)  # 繪製樣本點
    plt.title('DataSet')  # 繪製title
    plt.xlabel('X')
    plt.show()


def binSplitDataSet(dataSet, feature, value):
    """
    函數說明:根據特徵切分數據集合
    Parameters:
        dataSet - 數據集合
        feature - 帶切分的特徵
        value - 該特徵的值
    Returns:
        mat0 - 切分的數據集合0
        mat1 - 切分的數據集合1
    """
    mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
    mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
    return mat0, mat1


def regLeaf(dataSet):
    """
    函數說明:生成葉結點
    Parameters:
        dataSet - 數據集合
    Returns:
        目標變量的均值
    """
    return np.mean(dataSet[:, -1])


def regErr(dataSet):
    """
    函數說明:誤差估計函數
    Parameters:
        dataSet - 數據集合
    Returns:
        目標變量的總方差
    """
    return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]


def linearSolve(dataSet):  # helper function used in two places
    """
    將數據格式化爲目標變量Y和自變量X
    :param dataSet:
    :return:
    """
    m, n = np.shape(dataSet)
    X = np.mat(np.ones((m, n)));
    Y = np.mat(np.ones((m, 1)))  # create a copy of data with 1 in 0th postion
    X[:, 1:n] = dataSet[:, 0:n - 1];
    Y = dataSet[:, -1]  # and strip out Y
    xTx = X.T * X
    if np.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


def modelLeaf(dataSet):  # 當數據不在需要切分的時候,生成葉子節點模型
    ws, X, Y = linearSolve(dataSet)
    return ws


def modelErr(dataSet):  # 計算誤差
    ws, X, Y = linearSolve(dataSet)
    yHat = X * ws
    return sum(np.power(Y - yHat, 2))


def chooseBestSplit(dataSet, leafType=modelLeaf, errType=modelErr, ops=(1, 10)):
    """
    函數說明:找到數據的最佳二元切分方式函數
    Parameters:
        dataSet - 數據集合
        leafType - 生成葉結點
        regErr - 誤差估計函數
        ops - 用戶定義的參數構成的元組
    Returns:
        bestIndex - 最佳切分特徵
        bestValue - 最佳特徵值
    """
    import types
    # tolS允許的誤差下降值,tolN切分的最少樣本數
    tolS = ops[0];
    tolN = ops[1]
    # 如果當前所有值相等,則退出。(根據set的特性)
    if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
        return None, leafType(dataSet)
    # 統計數據集合的行m和列n
    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 = binSplitDataSet(dataSet, featIndex, splitVal)
            # 如果數據少於tolN,則退出
            if (np.shape(mat0)[0] < tolN) or (np.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)
    # 如果切分出的數據集很小則退出
    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, 10)):
    """
    函數說明:樹構建函數
    Parameters:
        dataSet - 數據集合
        leafType - 建立葉結點的函數
        errType - 誤差計算函數
        ops - 包含樹構建所有其他參數的元組
    Returns:
        retTree - 構建的迴歸樹
    """
    # 選擇最佳切分特徵和特徵值
    feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
    # r如果沒有特徵,則返回特徵值
    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


def isTree(obj):
    return (type(obj).__name__ == 'dict')

# 迴歸樹測試案例
# 爲了和 modelTreeEval() 保持一致,保留兩個輸入參數
# 模型效果計較
# 線性葉子節點 預測計算函數 直接返回 樹葉子節點 值
def regTreeEval(model, inDat):
    return float(model)

# 模型樹測試案例
# 對輸入數據進行格式化處理,在原數據矩陣上增加第1列,元素的值都是1,
# 也就是增加偏移值,和我們之前的簡單線性迴歸是一個套路,增加一個偏移量
def modelTreeEval(model, inDat):
    n = np.shape(inDat)[1]
    X = np.mat(np.ones((1, n + 1)))
    X[:, 1:n + 1] = inDat
    return float(X * model)

# 計算預測的結果
# 在給定樹結構的情況下,對於單個數據點,該函數會給出一個預測值。
# modelEval是對葉節點進行預測的函數引用,指定樹的類型,以便在葉節點上調用合適的模型。
# 此函數自頂向下遍歷整棵樹,直到命中葉節點爲止,一旦到達葉節點,它就會在輸入數據上
# 調用modelEval()函數,該函數的默認值爲regTreeEval()
def treeForeCast(tree, inData, modelEval=regTreeEval):
    """
        Desc:
            對特定模型的樹進行預測,可以是 迴歸樹 也可以是 模型樹
        Args:
            tree -- 已經訓練好的樹的模型
            inData -- 輸入的測試數據
            modelEval -- 預測的樹的模型類型,可選值爲 regTreeEval(迴歸樹) 或 modelTreeEval(模型樹),默認爲迴歸樹
        Returns:
            返回預測值
        """
    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 = np.mat(np.zeros((m, 1)))
    for i in range(m):
        yHat[i, 0] = treeForeCast(tree, np.mat(testData[i]), modelEval)
    return yHat

if __name__ == '__main__':
    train_filename = './data/exp2.txt'
    plotDataSet(train_filename)
    train_Data = loadDataSet(train_filename)
    train_Mat = np.mat(train_Data)
    myTree = createTree(train_Mat,modelLeaf, modelErr, ops=(1, 10))
    print(myTree)

    train_Data = loadDataSet('./data/bikeSpeedVsIq_train.txt')
    train_Mat = np.mat(train_Data)
    test_Data = loadDataSet('./data/bikeSpeedVsIq_test.txt')
    test_Mat = np.mat(test_Data)
    # 迴歸樹相關係數
    tree = createTree(train_Mat, ops=(1, 20))
    YHat = createForeCast(tree, test_Mat[:, 0], regTreeEval)
    corr = np.corrcoef(YHat, test_Mat[:, 1], rowvar=0)[0][1]
    print(corr)

    # 模型樹相關係數
    tree = createTree(train_Mat, modelLeaf, modelErr, ops=(1, 20))
    YHat = createForeCast(tree, test_Mat[:, 0], modelTreeEval)
    corr = np.corrcoef(YHat, test_Mat[:, 1], rowvar=0)[0][1]
    print(corr)

    # 線性模型相關係數
    ws, X, Y = linearSolve(train_Mat)
    for i in range(np.shape(test_Mat)[0]):
        YHat[i] = test_Mat[i, 0] * ws[1, 0] + ws[0, 0]
    corr = np.corrcoef(YHat, test_Mat[:, 1], rowvar=0)[0, 1]
    print(corr)

結果顯示:

{'spInd': 0, 'spVal': 0.285477, 'left': matrix([[1.69855694e-03],
        [1.19647739e+01]]), 'right': matrix([[3.46877936],
        [1.18521743]])}
普通迴歸樹 預測結果的相關係數R2: 0.964085
模型迴歸樹 預測結果的相關係數R2: 0.976041
線性迴歸模型 預測結果的相關係數R2: 0.943468

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章