1. 程式人生 > >經典演算法詳解--CART分類決策樹、迴歸樹和模型樹

經典演算法詳解--CART分類決策樹、迴歸樹和模型樹

Classification And Regression Tree(CART)是一種很重要的機器學習演算法,既可以用於建立分類樹(Classification Tree),也可以用於建立迴歸樹(Regression Tree),本文介紹了CART用於離散標籤分類決策和連續特徵迴歸時的原理。決策樹建立過程分析了資訊混亂度度量Gini指數、連續和離散特徵的特殊處理、連續和離散特徵共存時函式的特殊處理和後剪枝;用於迴歸時則介紹了迴歸樹和模型樹的原理、適用場景和建立過程。個人認為,迴歸樹和模型樹可以被看做“群落分類”演算法,群落內標籤值連續分佈,群落間有分界。

(一)認識CART演算法

Classification And Regression Tree(CART)是決策樹的一種,並且是非常重要的決策樹,屬於Top Ten Machine Learning Algorithm。顧名思義,CART演算法既可以用於建立分類樹(Classification Tree),也可以用於建立迴歸樹(Regression Tree)、模型樹(Model Tree),兩者在建樹的過程稍有差異。前文“機器學習經典演算法詳解及Python實現–決策樹(Decision Tree)”詳細介紹了分類決策樹原理以及ID3、C4.5演算法,本文在該文的基礎上詳述CART演算法在決策樹分類以及樹迴歸中的應用。
建立分類樹遞迴過程中,CART每次都選擇當前資料集中具有最小Gini資訊增益的特徵作為結點劃分決策樹。ID3演算法和C4.5演算法雖然在對訓練樣本集的學習中可以儘可能多地挖掘資訊,但其生成的決策樹分支、規模較大,CART演算法的二分法可以簡化決策樹的規模,提高生成決策樹的效率。對於連續特徵,CART也是採取和C4.5同樣的方法處理。為了避免過擬合(Overfitting),CART決策樹需要剪枝。預測過程當然也就十分簡單,根據產生的決策樹模型,延伸匹配特徵值到最後的葉子節點即得到預測的類別。
建立迴歸樹時,觀察值取值是連續的、沒有分類標籤,只有根據觀察資料得出的值來建立一個預測的規則。在這種情況下,Classification Tree的最優劃分規則就無能為力,CART則使用最小剩餘方差(Squared Residuals Minimization)來決定Regression Tree的最優劃分,該劃分準則是期望劃分之後的子樹誤差方差最小。建立模型樹,每個葉子節點則是一個機器學習模型,如線性迴歸模型。

CART演算法的重要基礎包含以下三個方面:
(1)二分(Binary Split):在每次判斷過程中,都是對觀察變數進行二分。
CART演算法採用一種二分遞迴分割的技術,演算法總是將當前樣本集分割為兩個子樣本集,使得生成的決策樹的每個非葉結點都只有兩個分枝。因此CART演算法生成的決策樹是結構簡潔的二叉樹。因此CART演算法適用於樣本特徵的取值為是或非的場景,對於連續特徵的處理則與C4.5演算法相似。
(2)單變數分割(Split Based on One Variable):每次最優劃分都是針對單個變數。
(3)剪枝策略:CART演算法的關鍵點,也是整個Tree-Based演算法的關鍵步驟。
剪枝過程特別重要,所以在最優決策樹生成過程中佔有重要地位。有研究表明,剪枝過程的重要性要比樹生成過程更為重要,對於不同的劃分標準生成的最大樹(Maximum Tree),在剪枝之後都能夠保留最重要的屬性劃分,差別不大。反而是剪枝方法對於最優樹的生成更為關鍵。

(二)CART分類決策樹

1,CART的資訊理論基礎和演算法過程

CART與C4.5的不同之處是節點分裂建立在GINI指數這個概念上,GINI指數主要是度量資料劃分或訓練資料集D的不純度為主。GINI值越小,表明樣本的純淨度越高(即該樣本只屬於同一類的概率越高)。衡量出資料集某個特徵所有取值的Gini指數後,就可以得到該特徵的Gini Split info,也就是GiniGain。不考慮剪枝情況下,分類決策樹遞迴建立過程中就是每次選擇GiniGain最小的節點做分叉點,直至子資料集都屬於同一類或者所有特徵用光了。
因為CART二分的特性,當訓練資料具有兩個以上的類別,CART需考慮將目標類別合併成兩個超類別,這個過程稱為雙化。超類別總如何進一步區分類別呢?根據別的特徵進一步分類?TBD
(1)Gini指數的概念:
GINI指數是一種不等性度量,通常用來度量收入不平衡,可以用來度量任何不均勻分佈,是介於0~1之間的數,0-完全相等,1-完全不相等。分類度量時,總體內包含的類別越雜亂,GINI指數就越大(跟熵的概念很相似)。
對於一個數據集T,其Gini計算方式為:

這裡寫圖片描述

2,對離散分佈、且取值數目>=3的特徵的處理:

正是因為CART樹是二叉樹,所以對於樣本的有N>=3個取值的離散特徵的處理時也只能有兩個分支,這就要通過組合人為的建立二取值序列並取GiniGain最小者作為樹分叉決策點。如某特徵值具有[‘young’,’middle’,’old’]三個取值,那麼二分序列會有如下3種可能性(空集和滿集在CART分類中沒有意義):
[((‘young’,), (‘middle’, ‘old’)), ((‘middle’,), (‘young’, ‘old’)), ((‘old’,), (‘young’, ‘middle’))]
採用CART演算法,就需要分別計算按照上述List中的二分序列做分叉時的Gini指數,然後選取產生最小的GINIGain的二分序列做該特徵的分叉二值序列參與樹構建的遞迴。如果某特徵取值有4個,那麼二分序列組合就有7種,5個取值就有15種組合,建立多值離散特徵二分序列組合可採用Python的itertools包,程式如下:

from itertools import *   
import pdb   
def featuresplit(features):   
    count = len(features)   
    featureind = range(count)   
    featureind.pop(0) #get value 1~(count-1)  
    combiList = []   
    for i in featureind:   
        com = list(combinations(features, len(features[0:i])))   
        combiList.extend(com)   
    combiLen = len(combiList)   
    featuresplitGroup = zip(combiList[0:combiLen/2], combiList[combiLen-1:combiLen/2-1:-1])   
    return featuresplitGroup   
if __name__ == '__main__':   
    test= range(3)   
    splitGroup = featuresplit(test)   
    print 'splitGroup', len(splitGroup), splitGroup   
    test= range(4)   
    splitGroup = featuresplit(test)   
    print 'splitGroup', len(splitGroup),splitGroup   
    test= range(5)   
    splitGroup = featuresplit(test)   
    print 'splitGroup', len(splitGroup),splitGroup   
    test= ['young','middle','old']   
    splitGroup = featuresplit(test)   
    print 'splitGroup', len(splitGroup),splitGroup 

因此CART不適用於離散特徵有多個取值可能的場景。此時,若定要使用CART,則最好預先人為的將離散特徵的取值縮減。
那麼對於二分後的左右分支,如果特徵取值tuple中元素多於2個,該特徵是否還要繼續參與當前子資料集的二分呢?

我認為需要,因此該特徵繼續參與分類決策樹遞迴,直至左右分支上該特徵的取值都是唯一的(即不再包含該特徵)。那麼離散特徵的datasplit函式就應該:如果按照當前分支特徵分叉後,分支上特徵取值tuple>=2,則分支子資料集保留該特徵,該tuple繼續參與上的樹構建的遞迴;否則分支子資料集刪除該特徵。

def splitDataSet(dataSet, axis, valueTuple):   
    '''return dataset satisfy condition dataSet[i][axis] == valueTuple, 
    and remove dataSet[i][axis] if len(valueTuple)==1'''  
    retDataSet = []   
    length = len(valueTuple)   
    if length ==1:   
      for featVec in dataSet:   
        if featVec[axis] == valueTuple[0]:   
            reducedFeatVec = featVec[:axis]     #chop out axis used for splitting  
            reducedFeatVec.extend(featVec[axis+1:])   
            retDataSet.append(reducedFeatVec)   
    else:   
      for featVec in dataSet:   
        if featVec[axis] in valueTuple:   
            retDataSet.append(featVec)   
    return retDataSet  

3,對連續特徵的處理

連續屬性參考C4.5的離散化過程,區別在於CART演算法中要以GiniGain最小作為分界點選取標準。是否需要修正?處理過程為:
先把連續屬性轉換為離散屬性再進行處理。雖然本質上屬性的取值是連續的,但對於有限的取樣資料它是離散的,如果有N條樣本,那麼我們有N-1種離散化的方法:<=vj的分到左子樹,>vj的分到右子樹。計算這N-1種情況下最大的資訊增益率。另外,對於連續屬性先進行排序(升序),只有在決策屬性(即分類發生了變化)發生改變的地方才需要切開,這可以顯著減少運算量。
(1) 對特徵的取值進行升序排序
(2) 兩個特徵取值之間的中點作為可能的分裂點,將資料集分成兩部分,計算每個可能的分裂點的GiniGain。優化演算法就是隻計算分類屬性發生改變的那些特徵取值
(3)選擇GiniGain最小的分裂點作為該特徵的最佳分裂點(注意,若修正則此處需對最佳分裂點的Gini Gain減去log2(N-1)/|D|(N是連續特徵的取值個數,D是訓練資料數目)
實現連續特徵資料集劃分的Python程式為(採用Numpy matrix,連續特徵取值就可以省略排序這一步了):

def binSplitDataSet(dataSet, feature, value):    
    mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]     
    mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]     
    return mat0,mat1

其中dataset為numpy matrix, feature為dataset連續特徵在dataset所有特徵中的index,value即為feature的一個取值。
必須注意的是:根據離散特徵分支劃分資料集時,子資料集中不再包含該特徵(因為每個分支下的子資料集該特徵的取值就會是一樣的,資訊增益或者Gini Gain將不再變化);而根據連續特徵分支時,各分支下的子資料集必須依舊包含該特徵(當然,左右分支各包含的分別是取值小於、大於等於分裂值的子資料集),因為該連續特徵再接下來的樹分支過程中可能依舊起著決定性作用。

4,訓練資料彙總離散特徵和連續特徵混合存在時的處理

C4.5和CART演算法決策樹建立過程中,由於離散特徵和連續特徵的處理函式不同。當訓練資料中兩種特徵並存時必須能夠識別分佈型別,從而呼叫相應的函式。那麼有兩種方法:
(1)每個特徵註明是連續分佈還是離散分佈,如0表示離散、1表示連續.如此訓練、決策時都可以分辨分佈型別。
(2)函式中根據特徵取值的個數判定,如featureValueCount>10(當然,離散特徵取值不可能這麼多)則為連續分佈,否則為離散分佈。此時構建的決策樹模型中,必須註明特徵的分佈型別(如構建一個List,長度為featureCount,其中元素0:離散,1:連續)。
Note:對於取值為是或者否的離散特徵,將其按離散或者連續分佈處理均可。按照連續分佈反而簡單,取std=0.5即可簡單的實現split。此時分佈判斷標準更改為featureValueCount>20 or ==2。
(3) 利用獨熱編碼(OneHotEncoding),Python sklearn 的preprocessing提供了OneHotEncoder()能夠將離散值轉換成連續值處理。獨熱編碼即 One-Hot 編碼,又稱一位有效編碼,其方法是使用N位狀態暫存器來對N個狀態進行編碼,每個狀態都由他獨立的暫存器位,並且在任意時候,其中只有一位有效。對於每一個特徵,如果它有m個可能值,那麼經過獨熱編碼後,就變成了m個二元特徵。並且,這些特徵互斥,每次只有一個啟用。因此,資料會變成稀疏的。這樣做的好處主要有:解決了分類器不好處理屬性資料的問題、在一定程度上也起到了擴充特徵的作用。參考‘OneHotEncoder進行資料預處理’。

5,CART的剪枝

分析分類迴歸樹的遞迴建樹過程,不難發現它實質上存在著一個數據過度擬合問題。在決策樹構造時,由於訓練資料中的噪音或孤立點,許多分枝反映的是訓練資料中的異常,使用這樣的判定樹對類別未知的資料進行分類,分類的準確性不高。因此試圖檢測和減去這樣的分支,檢測和減去這些分支的過程被稱為樹剪枝。樹剪枝方法用於處理過分適應資料問題。通常,這種方法使用統計度量,減去最不可靠的分支,這將導致較快的分類,提高樹獨立於訓練資料正確分類的能力。決策樹常用的剪枝常用的簡直方法有兩種:預剪枝(Pre-Pruning)和後剪枝(Post-Pruning)。預剪枝是根據一些原則及早的停止樹增長,如樹的深度達到使用者所要的深度、節點中樣本個數少於使用者指定個數、不純度指標下降的最大幅度小於使用者指定的幅度等;後剪枝則是通過在完全生長的樹上剪去分枝實現的,通過刪除節點的分支來剪去樹節點,可以使用的後剪枝方法有多種,比如:代價複雜性剪枝、最小誤差剪枝、悲觀誤差剪枝等等。
CART常採用事後剪枝方法,構建決策樹過程中的第二個關鍵就是用獨立的驗證資料集對訓練集生長的樹進行剪枝。TBD
關於後剪枝的具體理論可以參考“資料探勘十大經典演算法–CART: 分類與迴歸樹”剪枝部分。

6,Python實現CART決策樹

相對於ID3、C4.5決策樹演算法,CART演算法的的實現過程在結構上是類似的,區別在於:
(1)最佳特徵度量採取Gini Gain,因此calcShannonEnt方法要替換成calcGini方法
(2)CART採取二分法,因此對於有多個取值的離散特徵,需要首先獲取最小二分序列及其GiniGain,因此splitDataSet方法需按照取值tuple分開、chooseBestFetureToSplit要返回最佳分叉點及其二分序列如((‘middle’,), (‘young’, ‘old’))。
(3)決策樹模型判決演算法中對於離散特徵也要根據特徵值Tuple進行,即判斷特徵值取值屬於左分支還是有分支;對於連續特徵則是判斷特徵值取值是大於分裂值還是小於等於分裂值。

(三)CART迴歸樹和模型樹
當資料擁有眾多特徵並且特徵之間關係十分複雜時,構建全域性模型的想法就顯得太難了,也略顯笨拙。而且,實際生活中很多問題都是非線性的,不可能使用全域性線性模型來擬合任何資料。一種可行的方法是將資料集切分成很多份易建模的資料,然後利用線性迴歸技術來建模。如果首次切分後仍然難以擬合線性模型就繼續切分。在這種切分方式下,樹結構和迴歸法就相當有用。
迴歸樹與分類樹的思路類似,但葉節點的資料型別不是離散型,而是連續型,對CART稍作修改就可以處理迴歸問題。CART演算法用於迴歸時根據葉子是具體指還是另外的機器學習模型又可以分為迴歸樹和模型樹。但無論是迴歸樹還是模型樹,其適用場景都是:標籤值是連續分佈的,但又是可以劃分群落的,群落之間是有比較鮮明的區別的,即每個群落內部是相似的連續分佈,群落之間分佈確是不同的。所以迴歸樹和模型樹既算迴歸,也稱得上分類。
迴歸是為了處理預測值是連續分佈的情景,其返回值應該是一個具體預測值。迴歸樹的葉子是一個個具體的值,從預測值連續這個意義上嚴格來說,迴歸樹不能稱之為“迴歸演算法”。因為迴歸樹返回的是“一團”資料的均值,而不是具體的、連續的預測值(即訓練資料的標籤值雖然是連續的,但迴歸樹的預測值卻只能是離散的)。所以迴歸樹其實也可以算為“分類”演算法,其適用場景要具備“物以類聚”的特點,即特徵值的組合會使標籤屬於某一個“群落”,群落之間會有相對鮮明的“鴻溝”。如人的風格是一個連續分佈,但是卻又能“群分”成文藝、普通和2B三個群落,利用迴歸樹可以判斷一個人是文藝還是2B,但卻不能度量其有多文藝或者多2B。所以,利用迴歸樹可以將複雜的訓練資料劃分成一個個相對簡單的群落,群落上可以再利用別的機器學習模型再學習。
模型樹的葉子是一個個機器學習模型,如線性迴歸模型,所以更稱的上是“迴歸”演算法。利用模型樹就可以度量一個人的文藝值了。
迴歸樹和模型樹也需要剪枝,剪枝理論和分類樹相同。為了獲得最佳模型,樹剪枝常採用預剪枝和後剪枝結合的方法進行。
那麼如何利用CART構建迴歸樹或者模型樹呢?且聽下面細細道來。

1,迴歸樹-利用差值選擇分支特徵
樹迴歸中,為成功構建以分段常數為葉節點的樹,需要度量出資料的一致性。分類決策樹建立時會在給定節點時計算分類資料的混亂度。那麼如何計算連續型數值的混亂度呢? 事實上, 在連續資料集上計算混亂度是非常簡單的–度量按某一特徵劃分前後標籤資料總差值,每次選取使資料總差值最小的那個特徵做最佳分支特徵為了對正負差值同等看待,一般使用絕對值或平方值來代替上述差值)。為什麼選擇計算差值呢》差值越小,相似度越高,越可能屬於一個群落咯。那麼如果選取方差做差值,總方差的計算方法有兩種:
(1)計算資料集均值std,計算每個資料點與std的方差,然後n個點求和。
(2)計算資料集方差var,然後var_sum = var*n,n為資料集資料數目。Python Matrix中可以利用var方法求得資料集方差,因此該方法簡單、方便。
與Gini Gain對離散特徵和連續特徵的處理方法類似,多值離散特徵需要選擇最優二分序列,連續特徵則要找出最優分裂點。
那麼,每次最佳分支特徵的選取過程為:
function chooseBestSplitFeature()
(1)先令最佳方差為無限大bestVar=inf。
(2)依次計算根據某特徵(FeatureCount次迭代)劃分資料後的總方差currentVar(,計算方法為:劃分後左右子資料集的總方差之和),如果currentVar<bestVar,則bestVar=currentVar.
(3)返回最佳分支特徵、分支特徵值(離散特徵則為二分序列、連續特徵則為分裂點的值),左右分支子資料集。
2,採取線性迴歸預測偏差構建模型樹
用樹來對資料建模,除了把葉節點簡單地設定為常數值之外,還有一種方法是把葉節點設定為分段線性函式,這裡所謂的分段線性(piecewise linear)是指模型由多個線性片段組成,這就是模型樹。模型樹的可解釋性是它優於迴歸樹的特點之一。另外,模型樹也具有更髙的預測準確度。
模型樹的建立過程大體上與迴歸樹是一樣的,區別就在於遞迴過程中最佳分支特徵選取時差值的計算。對於模型樹:給定的資料集先用線性的模型來對它進行擬合,然後計算真實的目標值與模型預測值間的差值,將這些差值的平方求和就得到了所需的總差值,最後依然選取總差值最小的特徵做分支特徵。至於線性迴歸採用哪種解法,就要參看線性迴歸模型的求解了。