機器學習實戰-樹回歸

本章介紹一個新的叫做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()
    ...: 
Hello World
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()
使用多個Tkinter部件創建的樹管理器
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容