第九章 樹迴歸
CART演算法 迴歸與模型樹 樹減枝演算法 python中GUI的使用
線性迴歸需要擬合所有的樣本點(局部加權線性迴歸除外),當資料擁有眾多特徵並且特徵之間關係十分複雜時,就不可能使用全域線性模型來擬合任何資料。
將資料集切分成很多份易建模的資料,再用線性迴歸技術來建模可破。
本章介紹CART(Classification And Regression Trees, 分類迴歸樹)的樹構建演算法,可用於分類還可用於迴歸。
9.1 複雜資料的局部性建模
chap3的決策樹主要是不斷將資料切分成小資料集,直到所有目標變數完全相同,或者資料不能再切分為止。決策樹是一種貪心演算法,並不考慮能否達到全域最優。其構建演算法ID3演算法每次選取當前最佳的特徵來分割資料,並按照該特徵的所有可能取值來劃分,之後該特徵不會再起作用。另外一種方法是二元切分法,每次把資料集切成兩份,如果資料的某特徵等於切分所要求的值,那麼這些資料就進入樹的左子樹,反之右子樹。二元切分法可處理連續型特徵,節省樹的構建時間。
CART使用二元切分來處理連續型變數,應用廣泛。
9.2 連續型和離散型特徵的樹的構建
與chap3類似,用字典儲存樹的結構。包括4元素:
待切分的特徵 待切分的特徵值 左子樹。當不再需要切分的時候,可是個單個值。 右子樹。類似左子樹。
CART演算法可固定樹的資料結構,樹包含左鍵和右鍵,可以儲存 另一顆子樹或者單個值。
虛擬碼:
找到最佳的待切分特徵:
如果該節點不能再分,將該節點存為分葉節點
執行二元切分
在右子樹調用createTree()方法
在左子樹調用createTree()方法
coding:
#!/usr/bin/env python# coding=utf-8from numpy import *def loadDataSet(fileName): dataMat = [] fr = open(fileName) for line in fr.readlines(): curLine = line.strip().split("\n") fltLine = map(float, curLine) #將每行的每個元素映射為浮點數 dataMat.append(fltLine) return dataMatdef binSplitDataSet(dataSet, feature,value):#資料集合,待切分的特徵,特徵值,將資料集合切分得到兩個子集 mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0], :][0] mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0], :][0] return mat0, mat1def createTree(dataSet, leafType = regLeaf, errType = regErr, ops=(1,4)): feat, val = chooseBestSplit(dataSet, leafType, errType, ops) #將資料集進行切分 if feat == None: return val else: 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 retTreetestMat = mat(eye(4))mat0, mat1 = binSplitDataSet(testMat, 1, 0.5) #print testMatprint mat0print mat1
9.3 將CART演算法用於迴歸
迴歸樹假設分葉節點是常數值。這種策略認為資料中的複雜關係可以用樹結構來概括。
為成功構建以分段常數為分葉節點的樹,需要度量出資料的一致性。首先計算所有資料的均值,然後計算每條 資料的值到均值的差值,一般使用絕對值或平方差 來代替差值 ,類似方差計算,方差為平方誤差的均值(均方差),這裡需要計算平方誤差的總值(總方差),均方差(var函數)乘以資料集樣本點的個數可破。
構建樹
chooseBestSplit()函數目標是找到資料集切分的最佳位置,遍曆所有的特徵及其可能的取值來找到使誤差最小化的切分閾值。
虛擬碼:
對每個特徵:
對每個特徵值:
將資料集切分成兩份
計算切分的誤差
如果當前誤差小於當前最小誤差,那麼將當前切分設定為最佳切分並更新最小誤差 返回最佳切分的特徵和閾值
資料:
Figure 9-1: 實驗資料部分樣本資料
coding:
#==============迴歸樹的切分函數=============================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] #切分的最少樣本數 if len(set(dataSet[:,-1].T.tolist()[0]))==1: #如果目標值相等,退出 return None, leafType(dataSet) else: 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): #如果某個子集行數不大於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 (shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN): #如果切分出的資料集很小則退出 return None, leafType(dataSet) return bestIndex, bestValue #=====================================================================myDat = loadDataSet("ex00.txt")myMat = mat(myDat)print createTree(myMat)myDat1 = loadDataSet("ex0.txt")myMat1 = mat(myDat1)print createTree(myMat1)
切分效果:
Figure 9-2: 切分效果 9.4 樹剪枝
一棵樹如果節點過多,表明該模型可能對資料進行了”過擬合“。可使用測試集上交叉驗證來發現過擬合。
剪枝(pruning):降低決策樹的複雜度來避免過擬合。分為預剪枝(prepruning)和後剪枝(postpruning),後者需要使用訓練集和測試集。
預剪枝
樹構建演算法對輸入參數tolS和tolN非常敏感,通過不斷地修改停止條件來得到合理結果並不是很好的辦法。
後剪枝
後剪枝需要使用測試集。首先指定參數,使得構建出的樹足夠大,足夠複雜,便於剪枝。從上而下找到分葉節點,用測試集來判斷這些分葉節點合并是否能降低測試誤差。是則合并。
虛擬碼:
基於已有的樹切分測試資料:
如果存在任一子集是一棵樹,則在該子集遞迴剪枝過程
計算將當前兩個分葉節點合并後的誤差
計算不合并的誤差
如果合并會降低誤差的化,就將分葉節點合并
coding:
#=============迴歸樹剪枝函數==========================================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.0def prune(tree, testData): if shape(testData)[0] == 0: return getMean(tree) #if we have no test data collapse the tree if (isTree(tree['right']) or isTree(tree['left'])):#if the branches are not trees try to prune them 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 they are now both leafs, see if we can merge them 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 treemyTree = createTree(myMat2, ops=(0,1))myDatTest = loadDataSet("ex2test.txt")myMat2Test = mat(myDatTest)print prune(myTree, myMat2Test)
9.5 模型樹
用樹來對資料建模,除了把分葉節點設定為常數值外,還可以將其設定為分段線性函數,分段線性(piecewise linear)即模型由多個線性片段組成。 Figure 9-3: 用來測試模型樹構建函數的分段線性資料
可以設計兩條分別從0.0-0.3、從0.3~1.0的直線,得到兩個線性模型,即分段線性模型。
兩條直線比很多節點群組成一顆大樹更容易理解。模型樹的可解釋性是它優於迴歸樹的特點之一。模型樹也具有更高的預測準確度。利用樹產生演算法對資料進行切分,且每份切分資料都能很容易被線性模型所表示,關鍵在於找到最佳切分。
coding:
#==========模型樹的分葉節點產生函數=========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] #將X,Y中的資料格式化 xTx = X.T*X if linalg.det(xTx) == 0.0: raise NameError("This matrix is singular, cannot do inverse") ws = linalg.pinv(xTx)*(X.T*Y) ws = xTx.I*(X.T*Y) return ws, X, Ydef modelLeaf(dataSet): #當資料不再需要切分的時候它負責產生分葉節點模型 ws, X, Y = linearSolve(dataSet) return wsdef modelErr(dataSet): ws, X, Y = linearSolve(dataSet) yHat = X*ws return sum(power(Y - yHat,2)) #計算平方誤差myMat2 = mat(loadDataSet("exp2.txt"))#print myMat2,type(myMat2)print createTree(myMat2, modelLeaf, modelErr, (1,10))
效果:
Figure 9-4: 切分結果
兩個線性模型分別為y=3.468+1.185x和y=0.00168+11.964x,實際資料是由模型y=3.5+1.0x和y=0+12x再加上高斯噪音產生,可以看出效果還是不錯。 9.6 樣本:樹迴歸與標準迴歸的比較
計算模型樹、迴歸樹及其他模型效果,比較客觀的方法是計算相關係數,R*R值,Numpy中corrcoef(yHat, y, rowvar = 0)也即皮爾遜相關係數。
coding:
#================用樹迴歸進行預測的代碼=============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) #如果輸入單個資料或行向量,返回一個浮點值 else: 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)#多次調用treeForeCast函數,將結果以列的形式放到yHat變數中 return yHattrainMat = mat(loadDataSet("bikeSpeedVsIq_train.txt"))testMat = mat(loadDataSet("bikeSpeedVsIq_test.txt"))myTree = createTree(trainMat, ops=(1,20))yHat = createForeCast(myTree, testMat[:,0])print "迴歸樹的皮爾遜相關係數:",corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]myTree = createTree(trainMat, modelLeaf, modelErr,(1,20))yHat = createForeCast(myTree, testMat[:,0], modelTreeEval)print "模型樹的皮爾遜相關係數:",corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]ws, X, Y = linearSolve(trainMat)print "線性迴歸係數:",wsfor i in range(shape(testMat)[0]): yHat[i] = testMat[i,0]*ws[1,0] + ws[0,0]print "線性迴歸模型的皮爾遜相關係數:",corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
效果:
Figure 9-5: 迴歸樹、模型樹、簡單線性迴歸的皮爾遜相關係數
可以看出模型樹的結果比迴歸樹效果好。
9.7 使用Python的Tkinter庫建立GUI
本小結用到python的一個圖形化使用者介面(GUI, Graphical User Interface)架構——Tkinter。
Tkinter的GUI由一些小組件組成(Widge)組成。小組件:文字框、按鈕、標籤和複選按鈕等對象。 Figure 9-6: Tkinter的hello world
myLabel調用grid()方法時,把myLabel的位置告訴了布局管理器,grid()函數會把小組件安排在一個二維表格中。
coding:
#!/usr/bin/env python# coding=utf-8#用於構建樹管理器介面的Tkinter小組件from numpy import *from Tkinter import *import regTreesdef reDraw(tolS, tolN): passdef drawNewTree(): passroot = Tk()Label(root, text = "Plot Place Holder").grid(row=0, columnspan=3) #設定文本,第0行,距0的行值為3,Label(root, text = "tolN").grid(row=1, column=0)tolNentry = Entry(root) #Entry為允許單行文本輸入的文字框,設定文字框,再定位置第1行第1列,再插入數值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)#Botton按鈕,設定第1行第2列,列值為3chkBtnVar = IntVar() #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()
效果: Figure 9-7: 使用多個Tkinter組件建立的樹管理器
整合Matplotlib和Tkinter
matplotlib繪製的映像可以放到GUI上。matplotlib構建程式時包含一個前端,如plot、scatter函數,也同時建立了一個後端,用於實現繪圖和不同應用之間的介面,改變後端可以將映像繪製在PNG,PDF,SVG等格式的檔案上。matplotlib將後端設定為TkAgg,TkAgg可以在所選GUI架構上調用Agg,把Agg呈現在畫布上。
coding:
import matplotlibmatplotlib.use("TkAgg") #設定後端為TkAggfrom matplotlib.backends.backend_tkagg import FigureCanvasTkAggfrom matplotlib.figure import Figuredef reDraw(tolS,tolN): reDraw.f.clf() #清空之前的映像 reDraw.a = reDraw.f.add_subplot(111) #重新添加子圖 if chkBtnVar.get(): #檢查複選框是否選中,確定是模型樹還是迴歸樹 if tolN<2: tolN=2 myTree = regTrees.createTree(reDraw.rawDat, regTrees.modelLeaf, regTrees.modelErr,(tolS,tolN)) yHat = regTrees.createForeCast(myTree, reDraw.testDat, regTrees.modelTreeEval) else: myTree = regTrees.createTree(reDraw.rawDat, ops=(tolS,tolN)) yHat = regTrees.createForeCast(myTree, reDraw.testDat) reDraw.a.scatter(reDraw.rawDat[:,0], reDraw.rawDat[:,1],s=5) #畫真實值的散佈圖 reDraw.a.plot(reDraw.testDat,yHat,linewidth=2.0) #畫預測值的直線圖 reDraw.canvas.show()def getInputs(): #擷取使用者輸入的值,tolN期望得到整數值,tolS期望得到浮點數, try: tolN = int(tolNentry.get()) #在Entry組件調用get方法, except: tolN = 10 print "enter Integer for tolN" tolNentry.delete(0,END) tolNentry.insert(0,"10") try: tolS = float(tolSentry.get()) except: tolS = 1.0 print "enter Integer for tolS" tolSentry.delete(0,END) tolSentry.insert(0,"1.0") return tolN,tolSdef drawNewTree(): #有人點擊ReDraw按鈕時就會調用該函數 tolN,tolS = getInputs() #得到輸入框的值 reDraw(tolS,tolN) #調用reDraw函數root = Tk()reDraw.f = Figure(figsize=(5,4),dpi=100)reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)reDraw.canvas.show()reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)
效果:
Figure 9-8: 用treeExplore的GUI構建的迴歸樹
Figure 9-9: 模型樹。參數為tolN=1,tolS=0 9.8小結
資料集中輸入資料和目標變數呈非線性關係,可使用樹結構來對預測值分段,包括分段常數或分段直線。分葉節點使用分段常數則為迴歸樹,若為線性迴歸方程則為模型樹。