本章介紹一個新的叫做CART(分類回歸樹)的樹構建算法。該算法既可以用于分類還可以用于回歸,因此非常值得學習。
樹回歸
優點:可以對復雜和非線性的數據建模
缺點:結果不易理解
使用數據類型:數值型和標稱型數據
本章將構建兩種樹:第一種是9.4節的回歸樹,第二種是9.5節的模型樹。下面給出兩種樹構建算法中的一些公用代碼:
#createTree()
找到最佳的帶切分特征:
如果該節點不能再分,將該節點存為葉節點
執行二元切分
在右子樹調用createTree()方法
在左子樹調用createTree()方法
下面開始構建regTree.py代碼
from numpy import *
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float,curLine)#映射成浮點數
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):#數據集,待切分的特征,該特征的某個值
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]#nonzero()返回數組的非0部分
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
#下面兩行存疑,因為和課本的輸出結果不符,要改成上面兩行醬紫
# mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]#nonzero()返回數組的非0部分
# mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
return mat0,mat1
In [31]: import regTrees
...: testMat = mat(eye(4))
...: testMat
...:
Out[31]:
matrix([[ 1., 0., 0., 0.],
[ 0., 1., 0., 0.],
[ 0., 0., 1., 0.],
[ 0., 0., 0., 1.]])
In [32]: mat0,mat1 = regTrees.binSplitDataSet(testMat,1,0.5)
In [33]: mat0
Out[33]: matrix([[ 0., 1., 0., 0.]])
In [34]: mat1
Out[34]:
matrix([[ 1., 0., 0., 0.],
[ 0., 0., 1., 0.],
[ 0., 0., 0., 1.]])
切分成功
下面開始介紹chooseBestSplit(),該函數的目標是找到數據集切分的最佳位置
對每個特征:
對每個特征值:
將數據集切分成兩份
計算切分的誤差
如果當前誤差小于當前最小誤差,那么將當前切分設定為最佳切分并更新最小誤差
返回最佳切分的特征
#切分函數和創建樹函數,refLeaf和regErr要寫在前面,一開始犯了這個低級錯誤...
def regLeaf(dataSet):#生成葉子結點
return mean(dataSet[:,-1])
def regErr(dataSet):
return var(dataSet[:,-1]) * shape(dataSet)[0]#方差函數*樣本個數=總方差
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
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
#對創建葉節點的函數引用,總方差計算函數的引用,用戶定義的參數構成的元組
tolS = ops[0]; tolN = ops[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].T.A.tolist())[0]):
#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, regLeaf(dataSet)
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): #切分數據集過小
return None, leafType(dataSet)
return bestIndex,bestValue
In [10]: import regTrees
...: myDat = regTrees.loadDataSet('ex00.txt')
...: myMat = mat(myDat)
...: regTrees.createTree(myMat)
...:
Out[10]:
{'left': 1.0180967672413792,
'right': -0.044650285714285719,
'spInd': 0,
'spVal': 0.48813}
下面開始構建一顆回歸樹:
In [5]: import regTrees
...: myDat1 = regTrees.loadDataSet('ex0.txt')
...: myMat1 = mat(myDat1)
...: regTrees.createTree(myMat1)
...:
Out[5]:
{'left': {'left': {'left': 3.9871631999999999,
'right': 2.9836209534883724,
'spInd': 1,
'spVal': 0.797583},
'right': 1.980035071428571,
'spInd': 1,
'spVal': 0.582002},
'right': {'left': 1.0289583666666666,
'right': -0.023838155555555553,
'spInd': 1,
'spVal': 0.197834},
'spInd': 1,
'spVal': 0.39435}
到目前為止,已經完成回歸樹的構建,但是需要某種措施來檢查構建過程是否得當。下面介紹剪枝函數。
通過降低決策樹的復雜度來避免過擬合的過程稱為剪枝。函數chooseBestSplit()中的提前終止條件,實際上就是所謂的剪枝操作。另外一種剪枝需要使用測試集和訓練集,稱為后剪枝。
樹構建算法其實對輸入的參數tolS和tolN非常敏感,如果使用其他值將不太容易達到這么好的效果。
為了說明這一點,在Python提示符下輸入如下:
In [27]: regTrees.createTree(myMat,ops=(0,1))
Out[27]:
{'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': 1.035533,
'right': 1.077553,
'spInd': 0,
'spVal': 0.993349},
'right': {'left': 0.74420699999999995,
#太長省略。。。
這里構建的樹過于臃腫,它甚至為數據集中每個樣本都分配了一個葉節點。
In [30]: myDat2 = regTrees.loadDataSet('ex2.txt')
...: myMat2 = mat(myDat2)
...: regTrees.createTree(myMat2)
...:
Out[30]:
{'left': {'left': {'left': {'left': 105.24862350000001,
'right': 112.42895575000001,
'spInd': 0,
'spVal': 0.958512},
'right': {'left': {'left': {'left': {'left': 87.310387500000004,
'right': {'left': {'left': 96.452866999999998,
'right': {'left': 104.82540899999999,
'right': {'left': 95.181792999999999,
'right': 102.25234449999999,
'spInd': 0,
'spVal': 0.872883},
'spInd': 0,
'spVal': 0.892999},
'spInd': 0,
'spVal': 0.910975},
'right': 95.275843166666661,
'spInd': 0,
'spVal': 0.85497},
'spInd': 0,
'spVal': 0.944221},
'right': {'left': 81.110151999999999,
'right': 88.784498800000009,
'spInd': 0,
'spVal': 0.811602},
#省略部分
停止條件tolS對誤差的數量級十分敏感。如果在選項中花費時間并對上述誤差容忍度去平方值,或許也能得到僅有兩個葉子結點組成的樹:
In [32]: regTrees.createTree(myMat2,ops=(10000,4))
Out[32]:
{'left': 101.35815937735848,
'right': -2.6377193297872341,
'spInd': 0,
'spVal': 0.499171}
使用后剪枝需要將數據集分成測試集和訓練集。首先指定參數,使得構建出的樹足夠大、足夠復雜,便于剪枝。接下來從上而下找到葉節點,用測試集來判斷將這些葉節點合并是否能降低測試誤差。如果是的話就合并,函數prune()的偽代碼如下:
基于已有的樹切分測試數據:
如果存在任一子集是一棵樹,則在該子集遞歸剪枝過程
計算將當前兩個葉節點合并后的誤差
計算合并的誤差
如果合并會降低誤差的話,就將葉節點合并
In [33]: myTree = regTrees.createTree(myMat2,ops=(0,1))#創建最大的樹
In [34]: myDatTest = regTrees.loadDataSet('ex2test.txt')
In [35]: myMat2Test = mat(myDatTest)
In [36]: regTrees.prune(myTree,myMat2Test)
merging
merging
merging
#省略大量merging
merging
Out[36]:
{'left': {'left': {'left': {'left': 92.523991499999994,
'right': {'left': {'left': {'left': 112.386764,
'right': 123.559747,
'spInd': 0,
'spVal': 0.960398},
'right': 135.83701300000001,
'spInd': 0,
'spVal': 0.958512},
'right': 111.2013225,
'spInd': 0,
'spVal': 0.956951},
'spInd': 0,
'spVal': 0.965969},
'right': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': {'left': 96.41885225,
'right': 69.318648999999994,
'spInd': 0,
'spVal': 0.948822},
'right': {'left': {'left': 110.03503850000001,
'right': {'left': 65.548417999999998,
'right': {'left': 115.75399400000001,
#省略部分結果
可以看到大量的節點以及被減掉,但沒有像預期的那樣剪枝成兩部分,這說明后剪枝可能不如預剪枝有效。一般的,為了尋求最佳模型,可以同時使用兩種剪枝技術。
用樹來對數據建模,除了把葉節點簡單地設定為常數值之外,還有一種方法是把節點設定為分段線性函數,這里所謂的分段線性是指模型由多個線性片段組成。
In [39]: myMat2 = mat(regTrees.loadDataSet('exp2.txt'))
In [40]: regTrees.createTree(myMat2,regTree.modelLeaf,regTrees.modelErr,(1,10))
Traceback (most recent call last):
In [41]: regTrees.createTree(myMat2,regTrees.modelLeaf,regTrees.modelErr,(1,10))
Out[41]:
{'left': matrix([[ 1.69855694e-03],
[ 1.19647739e+01]]), 'right': matrix([[ 3.46877936],
[ 1.18521743]]), 'spInd': 0, 'spVal': 0.285477}
見課本圖9-5,與真實模型非常接近。
前面介紹了模型樹,回歸樹和一般的回歸方法,下面測試一下哪個模型最好。
In [39]: trainMat = mat(regTrees.loadDataSet('bikeSpeedVsIq_train.txt'))
...: testMat = mat(regTrees.loadDataSet('bikeSpeedVsIq_test.txt'))
...: myTree = regTrees.createTree(trainMat,ops=(1,20))
...: yHat = regTrees.createForeCast(myTree, testMat[:,0])
...: corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
...:
Out[39]: 0.96408523182221406
同樣的,再創建一顆模型樹:
In [40]: myTree = regTrees.createTree(trainMat,regTrees.modelLeaf,regTrees.modelErr,(1,20))
In [42]: yHat = regTrees.createForeCast(myTree,testMat[:,0],regTrees.modelTreeEval)
In [44]: corrcoef(yHat,testMat[:,1],rowvar=0)[0,1]
Out[44]: 0.97604121913805941
R2越接近1.0越好,所以從上面的結果我們可以看出,這里模型樹的結果比回歸樹的結果好。下面我們看標準的線性回歸效果如何:
In [5]: ws,X,Y = regTrees.linearSolve(trainMat)
In [6]: ws
Out[6]:
matrix([[ 37.58916794],
[ 6.18978355]])
為了得到測試集上所有的yHat預測值,在測試數據上循環運行:
In [12]: for i in range(shape(testMat)[0]):
...: yHat[i] = testMat[i,0]*ws[1,0] + ws[0,0]
...:
In [13]: corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
Out[13]: 0.94346842356747662
可以看到,該方法在R2值上表現上面兩種樹回歸方法。所以,樹回貴州預測復雜數據上會比簡單線性模型更有效。
Python中有很多GUI框架,其中一個易于使用的Tkinter,是隨Python的標準編譯版本發布的。下面是一個簡單的Hello World:
In [23]: from Tkinter import *
...:
...: root = Tk()
...:
...: myLabel = Label(root, text='Hello World')
...:
...: myLabel.grid()
...:
...: root.mainloop()
...:
from numpy import *
from Tkinter import *
import regTrees
def reDraw(rolS,tolN):
pass
def drawNewTree():
pass
root = Tk()
Label(root, text='Plot Place Holder').grid(row=0, columnspan=3)
Label(root, text='tolN').grid(row=1, column = 0)#行和列的位置
tolNentry = Entry(root)#輸入文本框
tolNentry.grid(row=1,column=1)
tolNentry.insert(0,'10')
Label(root, text="tolS").grid(row=2, column=0)
tolSentry = Entry(root)
tolSentry.grid(row=2, column=1)
tolSentry.insert(0,'1.0')
Button(root, text="ReDraw", command=drawNewTree).grid(row=1, column=2, rowspan=3)
chkBtnVar = IntVar()
chkBtn = Checkbutton(root, text="Model Tree", variable = chkBtnVar)#復選文本框
chkBtn.grid(row=3, column=0, columnspan=2)
reDraw.rawDat = mat(regTrees.loadDataSet('sine.txt'))
reDraw.testDat = arange(min(reDraw.rawDat[:,0]),max(reDraw.rawDat[:,0]),0.01)
reDraw(1.0, 10)
root.mainloop()