機器學習:決策樹ID3\C4.5\CART\隨機森林總結及python上的實現 (2)

本文主要根據Mitchell的機器學習總結歸納,圖片大多來源於此,同時結合網上搜索到的資料和幾篇較新的文獻,自己寫的決策樹總結,當中的python算法摘自《集體智慧編程》,算法可在python2.7環境下運行。(本來想自己寫的。。可是不懂數據結構的樹,寫的太不像樣了,所以就看完智慧編程自己盲打了一遍,順便了解了下樹這種數據結構)


首先:

# define 結點 節點  //咳咳,本文中的所有'結點'的正確寫法都是'節點',懶得改了 =_= 大家應該懂的。。。

決策樹是目前應用最廣的歸納推理算法之一,屬於歸納學習的一種。主要用於分類離散值,不過改進後的算法也能對連續型數值進行分類。決策樹對噪聲具有很好的魯棒性,常見的決策樹算法形式主要有ID3,C4.5,CART等。

考慮訓練數據如下:


其決策樹形式如下:



如圖:從上到下,把樣本從最上的根節點排列到最後的葉子節點,葉子節點的值即代表了樣本所屬的分類。每個方框節點代表了樣本的屬性,其下的各個分叉線上的字母即爲該屬性的各個可能值,不斷向下分叉直到分完所有訓練數據。


接下里說下決策樹用的算法。


一、決策樹常見算法

ID3

ID3是最簡單的決策樹形式。回顧上面的決策樹形式,你應該會發現一個疑點,那就是爲什麼我們選取了outlook作爲根節點,而不選用其他的,或者說其實選哪個根節點都沒事呢?

這就涉及到一個概念,那就是最優分類屬性。算法判斷outlook是最優分類屬性,他能使得這個決策樹長的最簡單。

最優分類屬性是通過信息增益比率來判斷的。介紹前先引入一個概念,叫信息熵。如下:



如公式,S表示訓練數據集合,i=1→c是目標概念可能的取值,如上面就是兩個no和yes,Pi表示屬於目標概念i的訓練數據個數佔S的比率。

信息熵是從熱力學的熵中引申來的,我們知道在熱力學中,熵表示的是一種無序狀態,即越無序熵越大,有序結構的熵爲0。而信息熵表示,一個東西他的不確定性越高,他的熵就越大,當他完全確定時,他的信息熵爲0。

舉個例子:

(前提:我們假設天氣預報給出的預測概率是絕對正確的。)

天氣預報說明天100%會下雨,好的那這句話所對應的信息熵就是 -1*log1=0,

天氣預報說明天50%的可能會下雨,50%的可能會是晴天,那他的信息熵就是-0.5*log0.5-0.5*log0.5=1,

天氣預報說明天25%可能下雨,25%可能晴天,50%可能陰天,那麼可以計算其信息熵爲1.5

從這個例子可以看出,當事物的不確定性上升的時候,他的信息熵會變大。


然後給出信息增益的定義:


公式中的第二項是S按照屬性A進行分類後,整體所具有的信息熵。value(A)表示A中具有的所有屬性的值,計算各個屬性值下的信息熵Sv在乘上Sv所含的樣本數比例,相加得到按A分類後的信息熵。兩者相減即爲信息增益Gain(S,A)。

在這裏還是舉個例子:

計算最上面的表格Enjoysport按屬性wind劃分後的信息增益如下:


然後說下我們爲什麼要引入信息增益的概念。從信息增益的公式就可以看出,當第二項的值很小的時候,即按照A屬性劃分後,整體的熵很小時,信息增益就會很大,這個時候我們說,按照A屬性來進行劃分,使得整體熵顯著變小了!信息增益就表示了按照屬性A劃分後整體熵的降低值。最開始的最優分類屬性便是採取信息增益來度量,將最大的選爲最優的分類屬性,之後每層都是如此,每次都選取最優的屬性,但是這種方法存在一個很嚴重的問題,即當一個屬性的值過多且沒有實際分類價值時,他的信息增益會特別大,使得決策樹成爲一個深度爲1,但是寬度很大的決策樹,如日期屬性年月日,如果將其劃入分類的話,最後會生成根節點爲日期,深度爲一的決策樹,但是這個決策樹的分類效果極其之差,當在驗證數據上進行驗證時,他幾乎無法進行分類。

所以我們定義信息增益比率來避免這種情況,如下:




公式中Si跟信息增益中的Si意思一樣,仍是屬於i的樣例個數。當屬性的分類過多時,他的splitInformation會變大,以此來平衡。但是他也存在一個問題:即,當S中某個分類Si的樣例過多,其他屬性值下分類過少時,即Si→S時,會使得Split→0,所以最後大家想了一個折中的辦法:先依次計算所有屬性的Gain,然後僅對那些Gain值超過平均值的計算GainRatio。


好了,總算扯完信息增益比率了。。所以最後的結論就是ID3運用Gain自上而下選取每個最優屬性作爲節點進行分類。而後面說到的Gainratio其實是C4.5所使用的了。


C4.5

C4.5其實就是ID3的擴展,最簡單的決策樹形式用來處理離散值輸入和輸出,於是人們擴展了決策樹的功能,並加入了其他的擴展,衍生出了C4,5。C 4.5比ID3多了一些特性:

1、可用於對連續型輸入或輸出進行擬合。雖說如此,其實也只是把連續的值劃分爲幾個區間來輸入而已。

2、可處理缺少某些屬性的數據。收集的數據常常會有某些屬性值是缺失的,就像醫院裏統計的病人的各種檢測數據,總會有A做了抽血化驗沒有尿檢,B做了尿檢卻沒有抽血化驗。這個時候信息增益就很難計算,所以我們會主動給這些數據“補上”缺失的屬性。一般有三種方法:①賦給他結點n下的訓練樣例中該屬性的最常見值②賦給他結點n下被分類爲他所屬的類裏該屬性的最常見值。③按照概率劃分。按照結點n下該屬性在已知數據中出現的概率劃分給其他未知的實例。

3、增加了對過度擬合的處理:錯誤率後修剪法和規則後修剪法。矯正過度擬合最常採用驗證集合來調整。①錯誤率後修剪法:對生成的決策樹自下而上,每次刪除一個節點,然後在驗證集合上進行驗證,如果修剪後的決策樹在驗證集合上得到了更小的錯誤率,那就說明這次修剪是有效的,剔除那個節點,否則決策樹不變,知道判斷完所有節點。②規則後修剪:將各個節點表示成if-then的規則化表示再進行修剪。如上面提到的那棵決策樹,最左一條支路用規則來表示可以寫成:iIF (Outlook=sunny) and (Humidity=high) ,THEN PlayTennis=No,像這樣將所有的支路表示成規則後,每次剔除一個規則就統計在驗證集合上的錯誤率情況,直到得到最優的決策樹。

一般來說第二種方法較常採用,因爲規則化後的形式比較有利於人的理解,而且每次剔除的規則只是某個屬性的一個值,但是第一種方法卻剔除了一個屬性下的所有屬性值,這是不太好的。

4、處理代價不同的屬性:某些屬性不好採集,或者屬性的採集需要付出很高的成本代價(人力或者金錢)時,我們通常只在需要可靠分類時才統計他們,當不需要時會盡量選用代價低的屬性來進行決策樹的生成。如下:


會使低代價的屬性被優先選取。




CART(classification and regression tree)

cart是一種二分遞歸分割樹。他每個結點下只有兩個屬性值(當一個屬性含有三個以上屬性值時,我們人爲的把他分爲兩類如:A和非A),

所以最後他會生成一個很規則的二叉樹

ID3和C4.5的分類基礎都是採用的信息熵衍生而來的信息增益,而cart使用的是基尼不純度(Gini impurity)

Gini在分類類別越雜亂的時候越大,類似於信息熵。其定義如下:


當採用Gini來選取最優分類屬性時,我們按照下面的計算公式來:


公式裏,value(A)代表A屬性裏所有可能的取值,pi表示value(A)裏各個分類的比率,分別計算他的Gini指數,Sa代表屬性A取a值時的實例個數,SA爲A屬性裏的總實例個數,其實就是整個訓練數據個數。

同樣舉個例子如下:計算上表中按照Wind分類的Gini_gain

其中

Wind=strong有6個實例,其中3個No,3個Yes,wind=weak有8個實例,其中2個No,6個Yes。

  Strong Weak
No 3 2
Yes 3 6

Gini(Strong)=1-(3/6)^2-(3/6)^2=0.5

Gini(Weak)=1-(2/8)^2-(6/8)^2=0.375

Gini_gain(Wind)=(6/14)*0.5+(8/14)*0.375

以上,最後也是選取Gini_gain最大的爲根節點,跟信息增益其實是類似的。


PS:注意這裏是CART二分遞歸,所以如果計算Outlook時,前期已經將數據劃分爲overcast和非overcast了。


隨機森林

隨機森林(RandomForest)並不是獨立於ID3,C4.5,cart之外的算法,他結合了數學上的隨機抽樣,運用自助法(boot-strap)重採樣,來達到一種更好的擬合。下面介紹自助法中的bagging模式,步驟如下:

①假設目前訓練數據D有N個實例,M個特徵,我們採用放回抽樣抽出n個數據,m個特徵組成一個訓練數據D1,再用同樣的方法抽出n、m組成D2,然後D3......Dn

②分別用這n組訓練數據,如採用C4.5分別生成一個決策樹。

③將每個決策樹的決策彙總,選擇當中佔比最高的決策作爲整個森林的決策結果。(這種方法使用最多)

③用驗證集合對n個決策樹進行投票,選中表現最好的決策樹。(投票的意思應該是看誰在驗證集合上分類出錯率最小)


另一種自助法採用模式是Adaboosting,有興趣可以百度下,他主要對每個實例分配一個概率來抽取。

由整個森林的投票來決定結果可以避免單棵樹過擬合的情況,使得決策更加合理,隨機森林近年來在各種數據挖掘大賽前幾名中出現的頻率極高,是一個很有潛力的算法。

對於第二個③,因爲採用了隨機抽樣來生成訓練數據,所以n個數據集裏有很大可能出現一個不包含噪聲或噪聲很少的數據集Di,由他生成的決策樹將在後來的投票中具有很高的票數。因此隨機森林的方法對噪聲有很好的魯棒性,而且能夠很大程度上避免過度擬合,因爲我們總是選取在驗證集合上表現最好的一棵決策樹。


python的scikit-learn包裏封裝了隨機森林,感興趣的同學可以瞭解下。scikit-learn裏封裝了很多分類器,幫使用者節省了很多時間,scikit-learn簡單到了甚至被稱作了toy play工具包了。


二、目前決策樹的研究進展


目前的決策樹研究方向主要有兩:①將決策樹與其他算法相融合②尋找新的構建決策樹的方法。

關於①融合,目前有決策樹跟ANN(神經網絡)、GA(遺傳算法)等,結合了不同算法的優點,如將決策樹轉化成神經網絡的結構,加速神經網絡的運算。關於②尋找新方法,前幾年提出了個CHAIN算法,不知道現在怎麼樣了

大家感興趣想深入瞭解的話可以去看看最新的文獻,哈哈


對了,關於算法的融合,不知道這個算不算,不過我再看到的時候着實驚豔了一把,當然這也是因爲我現在還比較小白的原因,=_=,早幾天前看了coursera上的機器學習基石,裏面在提到感知器時介紹了對於分類錯誤之後的懲罰,如某些錯誤是我們無論如何都想避免的,那我們就會在這個錯誤出現時賦予他較高的犯錯代價,以修改感知器的假設函數擬合。如:國家安全局進入需要指紋驗證,用感知器來進行分類即:驗證指紋合格或不合格。可能分類錯誤的情況有:指紋匹配但是機器提示不合格、指紋不匹配但是機器提示合格。大家一看就知道第二個錯誤是不能允許的,畢竟對於安全局這種地方,出現一次這樣的錯誤成本太高了,而出現第一個錯誤的管理者還可以罵娘一下再驗證一次。所以這裏我們就會賦予第二個錯誤以較高的權重代價,來引導感知器。

然後這幾天查文獻,發現決策樹裏面有的也用到這個概念了!他將某些決策犯錯的分類賦予了較高的代價,使得他在驗證數據集上會盡量選取犯這種錯誤較少的決策樹!當時對我的震驚是極大的,畢竟我纔剛看完各種算法的介紹,談互相之間的融合還太早,腦子轉不過來,這個時候看到這個,對我的觸動是極大的。算法真是博大精深啊

相信在實際的應用當中,算法之間的融合只會更多。


三、決策樹的python代碼實現

暫無

#!usr/bin/env python2
#-*- coding:utf-8 -*-


from math import log
from PIL import Image,ImageDraw
import zlib

my_data=[['slashdot','USA','yes',18,'None'],
['google','France','yes',23,'Premium'],
['digg','USA','yes',24,'Basic'],
['kiwitobes','France','yes',23,'Basic'],
['google','UK','no',21,'Premium'],
['(direct)','New Zealand','no',12,'None'],
['(direct)','UK','no',21,'Basic'],
['google','USA','no',24,'Premium'],
['slashdot','France','yes',19,'None'],
['digg','USA','no',18,'None'],
['google','UK','no',18,'None'],
['kiwitobes','UK','no',19,'None'],
['digg','New Zealand','yes',12,'Basic'],
['slashdot','UK','no',21,'None'],
['google','UK','yes',18,'Basic'],
['kiwitobes','France','yes',19,'Basic']]


#創建決策節點
class decidenode():
    def __init__(self,col=-1,value=None,result=None,tb=None,fb=None):
        self.col=col         #待檢驗的判斷條件所對應的列索引值
        self.value=value     #爲了使結果爲true,當前列要匹配的值
        self.result=result   #葉子節點的值
        self.tb=tb           #true下的節點
        self.fb=fb           #false下的節點


#對數值型和離散型數據進行分類
def DivideSet(rows,column,value):
    splitfunction=None
    if isinstance(value,int) or isinstance(value,float):
        splitfunction=lambda x :x>=value
    else:
        splitfunction=lambda x :x==value

    set1=[row for row in rows if splitfunction(row[column])]
    set2=[row for row in rows if not splitfunction(row[column])]
    return (set1,set2)


#計算數據所包含的實例個數
def UniqueCount(rows):
    result={}
    for row in rows:
        r=row[len(row)-1]
        result.setdefault(r,0)
        result[r]+=1
    return result

#計算Gini impurity
def GiniImpurity(rows):
    total=len(rows)
    counts=uniquecounts(rows)
    imp=0
    for k1 in counts:
        p1=float(counts[k1])/total
        for k2 in counts:
            if k1==k2: continue
            p2=float(counts[k2])/total
            imp+=p1*p2
    return imp

#計算信息熵Entropy
def entropy(rows):
    log2=lambda x:log(x)/log(2)
    results=UniqueCount(rows)
    # Now calculate the entropy
    ent=0.0
    for r in results.keys( ):
        p=float(results[r])/len(rows)
        ent=ent-p*log2(p)
    return ent

#計算方差(當輸出爲連續型的時候,用方差來判斷分類的好或壞,決策樹兩邊分別是比較大的數和比較小的數)
#可以通過後修剪來合併葉子節點
def variance(rows):
    if len(rows)==0:return 0
    data=[row[len(rows)-1] for row in rows]
    mean=sum(data)/len(data)
    variance=sum([(d-mean)**2 for d in data])/len(data)
    return variance


###############################################################33
#創建決策樹遞歸
def BuildTree(rows,judge=entropy):
    if len(rows)==0:return decidenode()   

    #初始化值
    best_gain=0
    best_value=None
    best_sets=None
    best_col=None   
    S=judge(rows)
    

    #獲得最好的gain
    for col in range(len(rows[0])-1):
        total_value={}    
        for row in rows:
            total_value[row[col]]=1
        for value in total_value.keys():
            (set1,set2)=DivideSet(rows,col,value)

            #計算信息增益,將最好的保存下來
            s1=float(len(set1))/len(rows)   
            s2=float(len(set2))/len(rows)          
            gain=S-s1*judge(set1)-s2*judge(set2)
            if gain > best_gain:
                best_gain=gain
                best_value=value
                best_col=col
                best_sets=(set1,set2)
    #創建節點
    if best_gain>0:
        truebranch=BuildTree(best_sets[0])
        falsebranch=BuildTree(best_sets[1])
        return decidenode(col=best_col,value=best_value,tb=truebranch,fb=falsebranch)
    else:
        return decidenode(result=UniqueCount(rows))


#打印文本形式的tree
def PrintTree(tree,indent=''):
    if tree.result!=None:
        print str(tree.result)
    else:
        print '%s:%s?' % (tree.col,tree.value)
        print indent,'T->',
        PrintTree(tree.tb,indent+'  ')
        print indent,'F->',
        PrintTree(tree.fb,indent+'  ')

def getwidth(tree):
    if tree.tb==None and tree.fb==None: return 1
    return getwidth(tree.tb)+getwidth(tree.fb)


def getdepth(tree):
    if tree.tb==None and tree.fb==None: return 0
    return max(getdepth(tree.tb),getdepth(tree.fb))+1


#打印圖表形式的tree
def drawtree(tree,jpeg='tree.jpg'):
    w=getwidth(tree)*100
    h=getdepth(tree)*100+120
    img=Image.new('RGB',(w,h),(255,255,255))
    draw=ImageDraw.Draw(img)
    drawnode(draw,tree,w/2,20)
    img.save(jpeg,'JPEG')


def drawnode(draw,tree,x,y):
    if tree.result==None:
    # Get the width of each branch
        w1=getwidth(tree.fb)*100
        w2=getwidth(tree.tb)*100
        # Determine the total space required by this node
        left=x-(w1+w2)/2
        right=x+(w1+w2)/2
        # Draw the condition string
        draw.text((x-20,y-10),str(tree.col)+':'+str(tree.value),(0,0,0))
        # Draw links to the branches
        draw.line((x,y,left+w1/2,y+100),fill=(255,0,0))
        draw.line((x,y,right-w2/2,y+100),fill=(255,0,0))
        # Draw the branch nodes
        drawnode(draw,tree.fb,left+w1/2,y+100)
        drawnode(draw,tree.tb,right-w2/2,y+100)
    else:
        txt=' \n'.join(['%s:%d'%v for v in tree.result.items( )])
        draw.text((x-20,y),txt,(0,0,0))



#對新實例進行查詢
def classify(observation,tree):
    if tree.result!=None: return tree.result
    else:
        v=observation[tree.col]
        branch=None
        if isinstance(v,int) or isinstance(v,float):
            if v>=tree.value:
                branch=tree.tb
            else:
                branch=tree.fb
        else:
            if v==value:
                branch=tree.tb
            else:
                branch=tree.fb
        return classify(observation,branch)


#後剪枝,設定一個閾值mingain來後剪枝,當合並後熵增加的值小於原來的值,就合併
def prune(tree,mingain):
    if tree.tb.result==None:
        prune(tree.tb,mingain)
    if tree.fb.result==None:
        prune(tree.fb,mingain)

    
    if tree.tb.result!=None and tree.fb.result!=None:
        tb1,fb1=[],[]
        for v,c in tree.tb.result.items():
            tb1+=[[v]]*c    #這裏是爲了跟row保持一樣的格式,因爲UniqueCount就是對這種進行的計算

        for v,c in tree.fb.result.items():
            fb1+=[[v]]*c

        delta=entropy(tb1+fb1)-(entropy(tb1)+entropy(fb1)/2)
        if delta<mingain:
            tree.tb,tree.fb=None,None
            tree.result=UniqueCount(tb1+fb1)

#對缺失屬性的數據進行查詢
def mdclassify(observation,tree):
    if tree.result!=None:
        return tree.result

    if observation[tree.col]==None:
        tb,fb=mdclassify(observation,tree.tb),mdclassify(observation,tree.fb)        #這裏的tr跟fr實際是這個函數返回的字典
        tbcount=sum(tb.values())
        fbcount=sum(fb.values())
        tw=float(tbcount)/(tbcount+fbcount)
        fw=float(fbcount)/(tbcount+fbcount)

        result={}
        for k,v in tb.items():
            result.setdefault(k,0)
            result[k]=v*tw
        for k,v in fb.items():
            result.setdefault(k,0)
            result[k]=v*fw
        return result

    else:
        v=observation[tree.col]
        branch=None
        if isinstance(v,int) or isinstance(v,float):
            if v>=tree.value:
                branch=tree.tb
            else:
                branch=tree.fb
        else:
            if v==tree.value:
                branch=tree.tb
            else:
                branch=tree.fb
        return mdclassify(observation,branch)



def main():                  #以下內容爲我測試決策樹的代碼
    a=BuildTree(my_data,0.01)
    print a

    PrintTree(a)
 #   drawtree(a,jpeg='treeview.jpg')
    prune(a,0.1)
    PrintTree(a)
    prune(a,1)
    PrintTree(a)
    
    mdclassify(['google','France',None,None],a)
    print mdclassify(['google','France',None,None],a)

    mdclassify(['google',None,'yes',None],a)
    print mdclassify(['google',None,'yes',None],a)


if __name__=='__main__':
    main()




小結:

介紹了決策樹的三種常見算法:基於信息熵的ID3、C4.5,基於Gini的CART,還有運用自助法的隨機森林。

講述了決策樹最近的研究進展,其實這也是其他算法的前進方向,即不同算法之間的互相融合。

給出了關於某個形式的決策樹實現代碼,在編寫代碼的過程中,體會到其實最重要的環節往往在數據預處理上,如何鑑別某些變量是無關變量,對缺失屬性的數據的處理,代價高昂的屬性是否要收集,這些實際的問題,纔是阻礙數據分析與挖掘的要點之一。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章