機器學習實戰筆記(Python實現)-09-樹回歸


----------------------------------------------------------------------------------------

本系列文章為《機器學習實戰》學習筆記,內容整理自書本,網絡以及自己的理解,如有錯誤歡迎指正。

源碼在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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM