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