第十二篇:深入學習高階非線性迴歸演算法 --- 樹迴歸系列演算法

穆晨發表於2017-01-19

前言

       前文討論的迴歸演算法都是全域性且針對線性問題的迴歸,即使是其中的區域性加權線性迴歸法,也有其弊端(具體請參考前文)

       採用全域性模型會導致模型非常的臃腫,因為需要計算所有的樣本點,而且現實生活中很多樣本都有大量的特徵資訊。

       另一方面,實際生活中更多的問題都是非線性問題。

       針對這些問題,有了樹迴歸系列演算法。

迴歸樹

       在先前決策樹的學習中,構建樹是採用的 ID3 演算法。在迴歸領域,該演算法就有個問題,就是派生子樹是按照所有可能值來進行派生。

       因此 ID3 演算法無法處理連續性資料。

       故可使用二元切分法,以某個特定值為界進行切分。在這種切分法下,子樹個數小於等於2。

       除此之外,再修改擇優原則夏農熵 (因為資料變為連續型的了),便可將樹構建成一棵可用於迴歸的樹,這樣一棵樹便叫做迴歸樹。

       構建迴歸樹的虛擬碼:

1 找到最佳的待切分特徵:
2     如果該節點不能再分,將此節點存為葉節點。
3     執行二元切分
4     左右子樹分別遞迴呼叫此函式

       二元切分的虛擬碼:

1 對每個特徵:
2     對每個特徵值:
3         將資料集切成兩份
4         計算切分誤差
5         如果當前誤差小於最小誤差,則更新最佳切分以及最小誤差。

       特別說明, (並直接建立葉節點)有三種情況:
              1. 特徵值劃分完畢
              2. 劃分子集太小
              3. 劃分後誤差改進不大
       這幾個操作被稱做 "預剪枝"。
  下面給出一個完整的迴歸樹的小程式:

  1 #!/usr/bin/env python
  2 # -*- coding:UTF-8 -*-
  3 
  4 '''
  5 Created on 20**-**-**
  6 
  7 @author: fangmeng
  8 '''
  9 
 10 from numpy import *
 11 
 12 def loadDataSet(fileName):
 13     '載入測試資料'
 14     
 15     dataMat = []
 16     fr = open(fileName)
 17     for line in fr.readlines():
 18         curLine = line.strip().split('\t')
 19         # 所有元素轉換為浮點型別(函式程式設計)
 20         fltLine = map(float,curLine)
 21         dataMat.append(fltLine)
 22     return dataMat
 23 
 24 #============================
 25 # 輸入:
 26 #        dataSet: 待切分資料集
 27 #        feature: 切分特徵序號
 28 #        value:    切分值
 29 # 輸出:
 30 #        mat0,mat1: 切分結果
 31 #============================
 32 def binSplitDataSet(dataSet, feature, value):
 33     '切分資料集'
 34     
 35     mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]
 36     mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]
 37     return mat0,mat1
 38 
 39 #========================================
 40 # 輸入:
 41 #        dataSet: 資料集
 42 # 輸出:
 43 #        mean(dataSet[:,-1]): 均值(也就是葉節點的內容)
 44 #========================================
 45 def regLeaf(dataSet):
 46     '生成葉節點'
 47     
 48     return mean(dataSet[:,-1])
 49 
 50 #========================================
 51 # 輸入:
 52 #        dataSet: 資料集
 53 # 輸出:
 54 #        var(dataSet[:,-1]) * shape(dataSet)[0]: 平方誤差
 55 #========================================
 56 def regErr(dataSet):
 57     '計算平方誤差'
 58     
 59     return var(dataSet[:,-1]) * shape(dataSet)[0]
 60 
 61 #========================================
 62 # 輸入:
 63 #        dataSet: 資料集
 64 #        leafType: 葉子節點生成器
 65 #        errType: 誤差統計器
 66 #        ops: 相關引數
 67 # 輸出:
 68 #        bestIndex: 最佳劃分特徵 
 69 #        bestValue: 最佳劃分特徵值
 70 #========================================
 71 def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
 72     '選擇最優劃分'
 73     
 74     # 獲得相關引數中的最大樣本數和最小誤差效果提升值
 75     tolS = ops[0]; 
 76     tolN = ops[1]
 77     
 78     # 如果所有樣本點的值一致,那麼直接建立葉子節點。
 79     if len(set(dataSet[:,-1].T.tolist()[0])) == 1: 
 80         return None, leafType(dataSet)
 81     
 82     m,n = shape(dataSet)
 83     # 當前誤差
 84     S = errType(dataSet)
 85     # 最小誤差
 86     bestS = inf; 
 87     # 最小誤差對應的劃分方式
 88     bestIndex = 0; 
 89     bestValue = 0
 90     
 91     # 對於所有特徵
 92     for featIndex in range(n-1):
 93         # 對於某個特徵的所有特徵值
 94         for splitVal in set(dataSet[:,featIndex]):
 95             # 劃分
 96             mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)
 97             # 如果劃分後某個子集中的個數不達標
 98             if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): continue
 99             # 當前劃分方式的誤差
100             newS = errType(mat0) + errType(mat1)
101             # 如果這種劃分方式的誤差小於最小誤差
102             if newS < bestS: 
103                 bestIndex = featIndex
104                 bestValue = splitVal
105                 bestS = newS
106     
107     # 如果當前劃分方式還不如不劃分時候的誤差效果
108     if (S - bestS) < tolS: 
109         return None, leafType(dataSet)
110     # 按照最優劃分方式進行劃分
111     mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
112     # 如果劃分後某個子集中的個數不達標
113     if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):
114         return None, leafType(dataSet)
115     
116     return bestIndex,bestValue
117 
118 #========================================
119 # 輸入:
120 #        dataSet: 資料集
121 #        leafType: 葉子節點生成器
122 #        errType: 誤差統計器
123 #        ops: 相關引數
124 # 輸出:
125 #        retTree: 迴歸樹
126 #========================================
127 def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):
128     '構建迴歸樹'
129     
130     # 選擇最佳劃分方式
131     feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
132     # feat為None的時候無需劃分返回葉子節點
133     if feat == None: return val #if the splitting hit a stop condition return val
134     
135     # 遞迴呼叫構建函式並更新樹
136     retTree = {}
137     retTree['spInd'] = feat
138     retTree['spVal'] = val
139     lSet, rSet = binSplitDataSet(dataSet, feat, val)
140     retTree['left'] = createTree(lSet, leafType, errType, ops)
141     retTree['right'] = createTree(rSet, leafType, errType, ops)
142     
143     return retTree  
144 
145 def test():
146     '展示結果'
147     
148     # 載入資料
149     myDat = loadDataSet('/home/fangmeng/ex0.txt')
150     # 構建迴歸樹
151     myDat = mat(myDat)
152     
153     print createTree(myDat)
154     
155     
156 if __name__ == '__main__':
157     test()

       測試結果:

迴歸樹的優化工作 - 剪枝

       在上面的程式碼中,遞迴的條件中已經加入了重重的 "剪枝" 工作。

       這些在建樹的時候的剪枝操作通常被成為預剪枝。這是很有很有必要的,經過預剪枝的樹幾乎就是沒有預剪枝樹的大小的百分之一甚至更小,而效能相差無幾

       而在樹建立完畢之後,基於訓練集和測試集能做更多更高效的剪枝工作,這些工作叫做 "後剪枝"。

       可見,剪枝是一項較大的工作量,是對樹非常關鍵的優化過程。

       後剪枝過程的虛擬碼如下:

1 基於已有的樹切分測試資料:
2     如果存在任一子集是一棵樹,則在該子集上遞迴該過程。
3     計算將當前兩個葉節點合併後的誤差
4     計算不合並的誤差
5     如果合併會降低誤差,則將葉節點合併。

       具體實現函式如下:

 1 #===================================
 2 # 輸入:
 3 #        obj: 判斷物件
 4 # 輸出:
 5 #        (type(obj).__name__=='dict'): 判斷結果
 6 #===================================
 7 def isTree(obj):
 8     '判斷物件是否為樹型別'
 9     
10     return (type(obj).__name__=='dict')
11 
12 #===================================
13 # 輸入:
14 #        tree: 處理物件
15 # 輸出:
16 #        (tree['left']+tree['right'])/2.0: 坍塌後的替代值
17 #===================================
18 def getMean(tree):
19     '坍塌處理'
20     
21     if isTree(tree['right']): tree['right'] = getMean(tree['right'])
22     if isTree(tree['left']): tree['left'] = getMean(tree['left'])
23     
24     return (tree['left']+tree['right'])/2.0
25   
26 #===================================
27 # 輸入:
28 #        tree: 處理物件
29 #        testData: 測試資料集
30 # 輸出:
31 #        tree: 剪枝後的樹
32 #===================================  
33 def prune(tree, testData):
34     '後剪枝'
35     
36     # 無測試資料則坍塌此樹
37     if shape(testData)[0] == 0: 
38         return getMean(tree)
39     
40     # 若左/右子集為樹型別
41     if (isTree(tree['right']) or isTree(tree['left'])):
42         # 劃分測試集
43         lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
44     # 在新樹新測試集上遞迴進行剪枝
45     if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet)
46     if isTree(tree['right']): tree['right'] =  prune(tree['right'], rSet)
47     
48     # 如果兩個子集都是葉子的話,則在進行誤差評估後決定是否進行合併。
49     if not isTree(tree['left']) and not isTree(tree['right']):
50         lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
51         errorNoMerge = sum(power(lSet[:,-1] - tree['left'],2)) +sum(power(rSet[:,-1] - tree['right'],2))
52         treeMean = (tree['left']+tree['right'])/2.0
53         errorMerge = sum(power(testData[:,-1] - treeMean,2))
54         if errorMerge < errorNoMerge: 
55             return treeMean
56         else: return tree
57     else: return tree

模型樹

       這也是一種很棒的樹迴歸演算法。

       該演算法將所有的葉子節點不是表述成一個值,而是對葉子部分節點建立線性模型。比如可以是最小二乘法的基本線性迴歸模型。

       這樣在葉子節點裡存放的就是一組線性迴歸係數了。非葉子節點部分構造就和迴歸樹一樣。

       這個是上面建立迴歸樹演算法的函式頭:

       createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):

       對於模型樹,只需要修改修改 leafType(葉節點構造器) 和 errType(誤差分析器) 的實現即可,分別對應如下modelLeaf 函式和 modelErr 函式:

 1 #=========================
 2 # 輸入:
 3 #        dataSet: 測試集
 4 # 輸出:
 5 #        ws,X,Y: 迴歸模型
 6 #=========================
 7 def linearSolve(dataSet):
 8     '輔助函式,用於構建線性迴歸模型。'
 9     
10     m,n = shape(dataSet)
11     X = mat(ones((m,n))); 
12     Y = mat(ones((m,1)))
13     X[:,1:n] = dataSet[:,0:n-1]; 
14     Y = dataSet[:,-1]
15     xTx = X.T*X
16     if linalg.det(xTx) == 0.0:
17         raise NameError('係數矩陣不可逆')
18     ws = xTx.I * (X.T * Y)
19     return ws,X,Y
20 
21 #=======================
22 # 輸入:
23 #       dataSet: 資料集
24 # 輸出:
25 #        ws: 迴歸係數
26 #=======================
27 def modelLeaf(dataSet):
28     '葉節點構造器'
29     
30     ws,X,Y = linearSolve(dataSet)
31     return ws
32 
33 #=======================================
34 # 輸入:
35 #       dataSet: 資料集
36 # 輸出:
37 #        sum(power(Y - yHat,2)): 平方誤差
38 #=======================================
39 def modelErr(dataSet):
40     '誤差分析器'
41     
42     ws,X,Y = linearSolve(dataSet)
43     yHat = X * ws
44     return sum(power(Y - yHat,2))

迴歸樹 / 模型樹的使用

       前面的工作主要介紹了兩種樹 - 迴歸樹,模型樹的構建,下面進一步學習如何利用這些樹來進行預測。

       當然,本質也就是遞迴遍歷樹

       下為遍歷程式碼,通過修改引數設定要使用並傳遞進來的是迴歸樹還是模型樹:

 1 #==============================
 2 # 輸入:
 3 #       model: 葉子
 4 #       inDat: 測試資料
 5 # 輸出:
 6 #        float(model): 葉子值
 7 #==============================
 8 def regTreeEval(model, inDat):
 9     '迴歸樹預測'
10     
11     return float(model)
12 
13 #==============================
14 # 輸入:
15 #       model: 葉子
16 #       inDat: 測試資料
17 # 輸出:
18 #        float(X*model): 葉子值
19 #==============================
20 def modelTreeEval(model, inDat):
21     '模型樹預測'
22     n = shape(inDat)[1]
23     X = mat(ones((1,n+1)))
24     X[:,1:n+1]=inDat
25     return float(X*model)
26 
27 #==============================
28 # 輸入:
29 #        tree: 待遍歷樹
30 #        inDat: 測試資料
31 #        modelEval: 葉子值獲取器
32 # 輸出:
33 #        分類結果
34 #==============================
35 def treeForeCast(tree, inData, modelEval=regTreeEval):
36     '使用迴歸/模型樹進行預測 (modelEval引數指定)'
37     
38     # 如果非樹型別,返回值。
39     if not isTree(tree): return modelEval(tree, inData)
40     
41     # 左遍歷
42     if inData[tree['spInd']] > tree['spVal']:
43         if isTree(tree['left']): return treeForeCast(tree['left'], inData, modelEval)
44         else: return modelEval(tree['left'], inData)
45         
46     # 右遍歷
47     else:
48         if isTree(tree['right']): return treeForeCast(tree['right'], inData, modelEval)
49         else: return modelEval(tree['right'], inData)

       使用方法非常簡單,將樹和要分類的樣本傳遞進去就可以了。如果是模型樹就將分類函式 treeForeCast 的第三個引數改為modelTreeEval即可。

       這裡就不再演示實驗具體過程了。

小結

       1. 選擇哪個迴歸方法,得看哪個方法的相關係數高。(可使用 corrcoef 函式計算)

       2. 樹的迴歸和分類演算法其實本質上都屬於貪心演算法,不斷去尋找區域性最優解。

       3. 關於迴歸的討論就先告一段落,接下來將進入到無監督學習部分。

相關文章