----------------------------------------------------------------------------------------
本系列文章為《機器學習實戰》學習筆記,內容整理自書本,網絡以及自己的理解,如有錯誤歡迎指正。
源碼在Python3.5上測試均通過,代碼及數據 --> https://github.com/Wellat/MLaction
----------------------------------------------------------------------------------------
1、連續和離散型特征的樹的構建
決策樹算法主要是不斷將數據切分成小數據集,直到所有目標變量完全相同,或者數據不能再切分為止。它是一種貪心算法,並不考慮能否達到全局最優。前面介紹的用ID3構建決策樹的算法每次選取當前最佳的特征來分割數據,並按照該特征的所有可能取值來划分,這種切分過於迅速,且不能處理連續性特征。另外一種方法是二元切分法,每次把數據集切成兩份,如果數據的某特征等於切分所要求的值,那么這些數據就進入樹的左子樹,反之右子樹。二元切分法可處理連續型特征,節省樹的構建時間。
這里依然使用字典來存儲樹的數據結構,該字典將包含以下4個元素:
- 待切分的特征
- 待切分的特征值
- 右子樹,不需切分時,也可是單個值
- 左子樹,右子樹類似
本章將構建兩種樹:第一種是第2節的回歸樹(regression tree),其每個葉節點包含單個值;第二種是第3節的模型樹(model tree),其每個葉節點包含一個線性方程。創建這兩種樹時,我們將盡量使得代碼之間可以重用。下面先給出兩種樹構建算法中的一些共用代碼。
1 from numpy import * 2 3 def loadDataSet(fileName): 4 ''' 5 讀取一個一tab鍵為分隔符的文件,然后將每行的內容保存成一組浮點數 6 ''' 7 dataMat = [] 8 fr = open(fileName) 9 for line in fr.readlines(): 10 curLine = line.strip().split('\t') 11 fltLine = map(float,curLine) 12 dataMat.append(fltLine) 13 return dataMat 14 15 def binSplitDataSet(dataSet, feature, value): 16 ''' 17 數據集切分函數 18 ''' 19 mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:] 20 mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:] 21 return mat0,mat1 22 23 def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)): 24 ''' 25 樹構建函數 26 leafType:建立葉節點的函數 27 errType:誤差計算函數 28 ops:包含樹構建所需其他參數的元組 29 ''' 30 #選擇最優的划分特征 31 #如果滿足停止條件,將返回None和某類模型的值 32 #若構建的是回歸樹,該模型是一個常數;如果是模型樹,其模型是一個線性方程 33 feat, val = chooseBestSplit(dataSet, leafType, errType, ops) 34 if feat == None: return val # 35 retTree = {} 36 retTree['spInd'] = feat 37 retTree['spVal'] = val 38 #將數據集分為兩份,之后遞歸調用繼續划分 39 lSet, rSet = binSplitDataSet(dataSet, feat, val) 40 retTree['left'] = createTree(lSet, leafType, errType, ops) 41 retTree['right'] = createTree(rSet, leafType, errType, ops) 42 return retTree
2、CART回歸樹
CART(Classification And Regression Trees, 分類回歸樹)是十分著名的樹構建算法,它使用二元切分來處理連續性變量,對其稍作修改就可處理回歸問題。
2.1 構建樹
①切分數據集並生成葉節點
給定某個誤差計算方法,chooseBestSplit()函數會找到數據集上最佳的二元切分方式,此外,該函數還要確定什么時候停止切分,一旦停止切分會生成一個葉節點。該函數偽代碼大致如下:
②計算誤差
這里采用計算數據的平方誤差。
Python代碼:
1 def regLeaf(dataSet): 2 '''負責生成葉節點''' 3 #當chooseBestSplit()函數確定不再對數據進行切分時,將調用本函數來得到葉節點的模型。 4 #在回歸樹中,該模型其實就是目標變量的均值。 5 return mean(dataSet[:,-1]) 6 7 def regErr(dataSet): 8 ''' 9 誤差估計函數,該函數在給定的數據上計算目標變量的平方誤差,這里直接調用均方差函數 10 ''' 11 return var(dataSet[:,-1]) * shape(dataSet)[0]#返回總方差 12 13 def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)): 14 ''' 15 用最佳方式切分數據集和生成相應的葉節點 16 ''' 17 #ops為用戶指定參數,用於控制函數的停止時機 18 tolS = ops[0]; tolN = ops[1] 19 #如果所有值相等則退出 20 if len(set(dataSet[:,-1].T.tolist()[0])) == 1: 21 return None, leafType(dataSet) 22 m,n = shape(dataSet) 23 S = errType(dataSet) 24 bestS = inf; bestIndex = 0; bestValue = 0 25 #在所有可能的特征及其可能取值上遍歷,找到最佳的切分方式 26 #最佳切分也就是使得切分后能達到最低誤差的切分 27 for featIndex in range(n-1): 28 for splitVal in set(dataSet[:,featIndex]): 29 mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal) 30 if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): continue 31 newS = errType(mat0) + errType(mat1) 32 if newS < bestS: 33 bestIndex = featIndex 34 bestValue = splitVal 35 bestS = newS 36 #如果誤差減小不大則退出 37 if (S - bestS) < tolS: 38 return None, leafType(dataSet) 39 mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue) 40 #如果切分出的數據集很小則退出 41 if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): 42 return None, leafType(dataSet) 43 #提前終止條件都不滿足,返回切分特征和特征值 44 return bestIndex,bestValue
主要測試命令:
>>> reload(regTrees) >>> myData = regTrees.loadDataSet('ex00.txt') >>> myMat = mat(myData) >>> regTrees.createTree(myMat)
【注意】本代碼在Python3.5環境下測試未通過,錯誤發生在以上第5行-->return mean(dataSet[:,-1])
錯誤類型為 TypeError: unsupported operand type(s) for /: 'map' and 'int' 暫未找到解決辦法。所以,以下測試結果均來自書本。
2.2 剪枝
一棵樹如果節點過多,表明該模型可能對數據進行了“過擬合”。通過降低決策樹的復雜度來避免過擬合的過程稱為剪枝(pruning) 。
①預剪枝
在函數chooseBestSplit()中的提前終止條件,實際上是在進行一種所謂的預剪枝(prepruning)操作。樹構建算法其實對輸人的參數tols和tolN非常敏感,如果使用其他值將不太容易達到這么好的效果。
②后剪枝
使用后剪枝方法需要將數據集分成測試集和訓練集。首先指定參數,使得構建出的樹足夠大、足夠復雜,便於剪枝。接下來從上而下找到葉節點,用測試集來判斷將這些葉節點合並是否能降低測試誤差。如果是的話就合並 。
Python實現代碼:
1 def prune(tree, testData): 2 '''回歸樹剪枝函數''' 3 if shape(testData)[0] == 0: return getMean(tree) #無測試數據則返回樹的平均值 4 if (isTree(tree['right']) or isTree(tree['left'])):# 5 lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal']) 6 if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet) 7 if isTree(tree['right']): tree['right'] = prune(tree['right'], rSet) 8 #如果兩個分支已經不再是子樹,合並它們 9 #具體做法是對合並前后的誤差進行比較。如果合並后的誤差比不合並的誤差小就進行合並操作,反之則不合並直接返回 10 if not isTree(tree['left']) and not isTree(tree['right']): 11 lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal']) 12 errorNoMerge = sum(power(lSet[:,-1] - tree['left'],2)) +\ 13 sum(power(rSet[:,-1] - tree['right'],2)) 14 treeMean = (tree['left']+tree['right'])/2.0 15 errorMerge = sum(power(testData[:,-1] - treeMean,2)) 16 if errorMerge < errorNoMerge: 17 print("merging") 18 return treeMean 19 else: return tree 20 21 def isTree(obj): 22 '''判斷輸入變量是否是一棵樹''' 23 return (type(obj).__name__=='dict') 24 25 def getMean(tree): 26 '''從上往下遍歷樹直到葉節點為止,計算它們的平均值''' 27 if isTree(tree['right']): tree['right'] = getMean(tree['right']) 28 if isTree(tree['left']): tree['left'] = getMean(tree['left']) 29 return (tree['left']+tree['right'])/2.0
測試命令:
reload(regTrees) myData2 = regTrees.loadDataSet('ex2.txt') myMat2 = mat(myData2) from numpy import * myMat2 = mat(myData2) regTrees.createTree(myMat2) myTree = regTrees.createTree(myMat2, ops=(0,1)) myDataTest = regTrees.loadDataSet('ex2test.txt') myMat2Test = mat(myDataTest) regTrees.prune(myTree, myMat2Test)
3、模型樹
①葉節點
用樹建模,除了把葉節點簡單地設定為常數值外,還可把葉節點設定為分段線性函數,這里的分段線性是指模型由多個線性片段組成。
如下圖所示數據,如果使用兩條直線擬合是否比使用一組常數來建模好呢?答案顯而易見。可以設計兩條分別從0.0~0.3、從0.3~1.0的直線,於是就可以得到兩個線性模型。因為數據集里的一部分數據(0.0~0.3)以某個線性模型建模,而另一部分數據(0.3~1.0)則以另一個線性模型建模,因此我們說采用了所謂的分段線性模型。
②誤差計算
前面用於回歸樹的誤差計算方法這里不能再用。稍加變化,對於給定的數據集,先用線性的模型來對它進行擬合,然后計算真實的目標值與模型預測值間的差值。最后將這些差值的平方求和就得到了所需的誤差。
與回歸樹不同,模型樹Python代碼有以下變化:
1 def linearSolve(dataSet): 2 '''將數據集格式化成目標變量Y和自變量X,X、Y用於執行簡單線性回歸''' 3 m,n = shape(dataSet) 4 X = mat(ones((m,n))); Y = mat(ones((m,1))) 5 X[:,1:n] = dataSet[:,0:n-1]; Y = dataSet[:,-1]#默認最后一列為Y 6 xTx = X.T*X 7 #若矩陣的逆不存在,拋異常 8 if linalg.det(xTx) == 0.0: 9 raise NameError('This matrix is singular, cannot do inverse,\n\ 10 try increasing the second value of ops') 11 ws = xTx.I * (X.T * Y)#回歸系數 12 return ws,X,Y 13 14 def modelLeaf(dataSet): 15 '''負責生成葉節點模型''' 16 ws,X,Y = linearSolve(dataSet) 17 return ws 18 19 def modelErr(dataSet): 20 '''誤差計算函數''' 21 ws,X,Y = linearSolve(dataSet) 22 yHat = X * ws 23 return sum(power(Y - yHat,2))
測試命令:
>>> regTrees.createTree(myMat,regTrees.modelLeaf,regTrees.modelErr.(1,10))
4、實例:樹回歸與標准回歸的比較
前面介紹了模型樹、回歸樹和一般的回歸方法,下面測試一下哪個模型最好。這些模型將在某個數據上進行測試,該數據涉及人的智力水平和自行車的速度的關系。
1 def createForeCast(tree, testData, modelEval=regTreeEval): 2 # 多次調用treeForeCast()函數,以向量形式返回預測值,在整個測試集進行預測非常有用 3 m=len(testData) 4 yHat = mat(zeros((m,1))) 5 for i in range(m): 6 yHat[i,0] = treeForeCast(tree, mat(testData[i]), modelEval) 7 return yHat 8 9 def treeForeCast(tree, inData, modelEval=regTreeEval): 10 ''' 11 # 在給定樹結構的情況下,對於單個數據點,該函數會給出一個預測值。 12 # modeEval是對葉節點進行預測的函數引用,指定樹的類型,以便在葉節點上調用合適的模型。 13 # 此函數自頂向下遍歷整棵樹,直到命中葉節點為止,一旦到達葉節點,它就會在輸入數據上 14 # 調用modelEval()函數,該函數的默認值為regTreeEval() 15 ''' 16 if not isTree(tree): return modelEval(tree, inData) 17 if inData[tree['spInd']] > tree['spVal']: 18 if isTree(tree['left']): return treeForeCast(tree['left'], inData, modelEval) 19 else: return modelEval(tree['left'], inData) 20 else: 21 if isTree(tree['right']): return treeForeCast(tree['right'], inData, modelEval) 22 else: return modelEval(tree['right'], inData) 23 24 def regTreeEval(model, inDat): 25 #為了和modeTreeEval()保持一致,保留兩個輸入參數 26 return float(model) 27 28 def modelTreeEval(model, inDat): 29 #對輸入數據進行格式化處理,在原數據矩陣上增加第0列,元素的值都是1 30 n = shape(inDat)[1] 31 X = mat(ones((1,n+1))) 32 X[:,1:n+1]=inDat 33 return float(X*model)
測試命令:
#回歸樹 >>> reload(regTrees) >>> 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) array([[ 1. , 0.96408523], [ 0.96408523, 1. ]]) #模型樹 >>> myTree = regTrees.createTree(trainMat, regTrees.modelLeaf, regTrees.modelErr , (1,20)) >>> yHat = regTrees.createForeCast(myTree, testMat[:,0], regTrees.modelTreeEval) >>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1] 0.97604121913806285 # 標准回歸 >>> ws, X, Y = regTrees.linearSolve(trainMat) >>> ws matrix([[ 37.58916794], [ 6.18978355]]) >>> for i in range(shape(testMat)[0]) : ... yHat[i] = testMat[i,0]*ws[1,0] + ws[0,0] ... >>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1] 0.94346842356747584
THE END.