1. 程式人生 > >決策樹ID3;C4.5詳解和python實現與R語言實現比較

決策樹ID3;C4.5詳解和python實現與R語言實現比較

本文網址:http://blog.csdn.net/crystal_tyan/article/details/42130851(請不要在採集站閱讀)

把決策樹研究一下,找來了一些自己覺得還可以的資料:

分類樹(決策樹)是一種十分常用的分類方法。他是一種監管學習,所謂監管學習說白了很簡單,就是給定一堆樣本,每個樣本都有一組屬性和一個類別,這些類別是事先確定的,那麼通過學習得到一個分類器,這個分類器能夠對新出現的物件給出正確的分類。這樣的機器學習就被稱之為監督學習。分類本質上就是一個map的過程。C4.5分類樹就是決策樹演算法中最流行的一種。下面給出一個數據集作為演算法例子的基礎,比如有這麼一個數據集,如下:


這個Golf資料集就是我們這篇部落格討論的基礎。我們分類的目的就是根據某一天的天氣狀態,如天氣,溫度,溼度,是否颳風,來判斷這一天是否適合打高爾夫球。

上面的圖片已經很明顯得說明了決策樹的原理了 下面說說資訊增益和熵

1. 資訊理論裡的熵

因此先回憶一下資訊理論中有關資訊量(就是“熵”)的定義。說有這麼一個變數X,它可能的取值有n多種,分別是x1,x2,……,xn,每一種取到的概率分別是P1,P2,……,Pn,那麼X的熵就定義為:

意思就是一個變數可能的變化越多(反而跟變數具體的取值沒有任何關係,只和值的種類多少以及發生概率有關),它攜帶的資訊量就越大(因此我一直覺得我們的政策法規資訊量非常大,因為它變化很多,基本朝令夕改,笑)。

2. 分類系統裡的熵

對分類系統來說,類別C是變數,它可能的取值是C1,C2,……,Cn,而每一個類別出現的概率是P(C1),P(C2),……,P(Cn),因此n就是類別的總數。此時分類系統的熵就可以表示為:

有同學說不好理解呀,這樣想就好了,文字分類系統的作用就是輸出一個表示文字屬於哪個類別的值,而這個值可能是C1,C2,……,Cn,因此這個值所攜帶的資訊量就是上式中的這麼多。

3. 資訊增益和熵的關係

資訊增益是針對一個一個的特徵而言的,就是看一個特徵t,系統有它和沒它的時候資訊量各是多少,兩者的差值就是這個特徵給系統帶來的資訊量,即增益。系統含有特徵t的時候資訊量很好計算,就是剛才的式子,它表示的是包含所有特徵時系統的資訊量。

問題是當系統不包含t時,資訊量如何計算?我們換個角度想問題,把系統要做的事情想象成這樣:說教室裡有很多座位,學生們每次上課進來的時 候可以隨便坐,因而變化是很大的(無數種可能的座次情況);但是現在有一個座位,看黑板很清楚,聽老師講也很清楚,於是校長的小舅子的姐姐的女兒託關係 (真輾轉啊),把這個座位定下來了,每次只能給她坐,別人不行,此時情況怎樣?對於座次的可能情況來說,我們很容易看出以下兩種情況是等價的:(1)教室 裡沒有這個座位;(2)教室裡雖然有這個座位,但其他人不能坐(因為反正它也不能參與到變化中來,它是不變的)。

對應到我們的系統中,就是下面的等價:(1)系統不包含特徵t;(2)系統雖然包含特徵t,但是t已經固定了,不能變化。

我們計算分類系統不包含特徵t的時候,就使用情況(2)來代替,就是計算當一個特徵t不能變化時,系統的資訊量是多少。這個資訊量其實也有專門的名稱,就叫做“條件熵”,條件嘛,自然就是指“t已經固定“這個條件。

但是問題接踵而至,例如一個特徵X,它可能的取值有n多種(x1,x2,……,xn), 當計算條件熵而需要把它固定的時候,要把它固定在哪一個值上呢?答案是每一種可能都要固定一下,計算n個值,然後取均值才是條件熵。而取均值也不是簡單的 加一加然後除以n,而是要用每個值出現的概率來算平均(簡單理解,就是一個值出現的可能性比較大,固定在它上面時算出來的資訊量佔的比重就要多一些)。

因此有這樣兩個條件熵的表示式:

這是指特徵X被固定為值xi時的條件熵,

這是指特徵X被固定時的條件熵,注意與上式在意義上的區別。從剛才計算均值的討論可以看出來,第二個式子與第一個式子的關係就是:

具體到我們文字分類系統中的特徵t,t有幾個可能的值呢?注意t是指一個固定的特徵,比如他就是指關鍵詞“經濟”或者“體育”,當我們說特徵“經濟”可能的取值時,實際上只有兩個,“經濟”要麼出現,要麼不出現。一般的,t的取值只有t(代表t出現)和clip_image006(代表t不出現),注意系統包含t但t 不出現與系統根本不包含t可是兩回事。

因此固定t時系統的條件熵就有了,為了區別t出現時的符號與特徵t本身的符號,我們用T代表特徵,而用t代表T出現,那麼:

與剛才的式子對照一下,含義很清楚對吧,P(t)就是T出現的概率,就是T不出現的概率。這個式子可以進一步展開,其中的

另一半就可以展開為:

因此特徵T給系統帶來的資訊增益就可以寫成系統原本的熵與固定特徵T後的條件熵之差:

公式中的東西看上去很多,其實也都很好計算。比如P(Ci),表示類別Ci出現的概率,其實只要用1除以類別總數就得到了(這是說你平等的看待每個類別而忽略它們的大小時這樣算,如果考慮了大小就要把大小的影響加進去)。再比如P(t),就是特徵T出現的概率,只要用出現過T的文件數除以總文件數就可以了,再比如P(Ci|t)表示出現T的時候,類別Ci出現的概率,只要用出現了T並且屬於類別Ci的文件數除以出現了T的文件數就可以了。

從以上討論中可以看出,資訊增益也是考慮了特徵出現和不出現兩種情況,與開方檢驗一樣,是比較全面的,因而效果不錯。但資訊增益最大的問題 還在於它只能考察特徵對整個系統的貢獻,而不能具體到某個類別上,這就使得它只適合用來做所謂“全域性”的特徵選擇(指所有的類都使用相同的特徵集合),而 無法做“本地”的特徵選擇(每個類別有自己的特徵集合,因為有的詞,對這個類別很有區分度,對另一個類別則無足輕重)。

================

一個例子:

================

任務:

根據天氣預測否去打網球

資料:

這個資料集來自Mitchell的機器學習,叫做是否去打網球play-tennis,以下資料仍然是從帶逗號分割的文字檔案,複製到紀事本,把字尾直接改為.csv就可以拿Excel開啟:

*play-tennis data,其中6個變數依次為:編號、天氣{Sunny、Overcast、Rain}、溫度{熱、冷、適中}、溼度{高、正常}、風力{強、弱}以及最後是否去玩的決策{是、否}。一個建議是把這些資料匯入Excel後,另複製一份去掉變數的資料到另外一個工作簿,即只保留14個觀測值。這樣可以方便地使用Excel的排序功能,隨時檢視每個變數的取值到底有多少。*/

NO. , Outlook , Temperature , Humidity , Wind , Play 
1 , Sunny , Hot , High , Weak , No 
2 , Sunny , Hot , High , Strong , No 
3 , Overcast , Hot , High , Weak , Yes 
4 , Rain , Mild , High , Weak , Yes 
5 , Rain , Cool , Normal , Weak , Yes 
6 , Rain , Cool , Normal , Strong , No 
7 , Overcast , Cool , Normal , Strong , Yes 
8 , Sunny , Mild , High , Weak , No 
9 , Sunny , Cool , Normal , Weak , Yes 
10 , Rain , Mild , Normal , Weak , Yes 
11 , Sunny , Mild , Normal , Strong , Yes 
12 , Overcast , Mild , High , Strong , Yes 
13 , Overcast , Hot , Normal , Weak , Yes 
14 , Rain , Mild , High , Strong , No

用決策樹來預測:

決策樹的形式類似於“如果天氣怎麼樣,去玩;否則,怎麼著怎麼著”的樹形分叉。那麼問題是用哪個屬性(即變數,如天氣、溫度、溼度和風力)最適合充當這顆樹的根節點,在它上面沒有其他節點,其他的屬性都是它的後續節點。

那麼借用上面所述的能夠衡量一個屬性區分以上資料樣本的能力的“資訊增益”(Information Gain)理論。

如果一個屬性的資訊增益量越大,這個屬性作為一棵樹的根節點就能使這棵樹更簡潔,比如說一棵樹可以這麼讀成,如果風力弱,就去玩;風力強,再按天氣、溫度等分情況討論,此時用風力作為這棵樹的根節點就很有價值。如果說,風力弱,再又天氣晴朗,就去玩;如果風力強,再又怎麼怎麼分情況討論,這棵樹相比就不夠簡潔了。

用熵來計算資訊增益:

1 計算分類系統熵
類別是 是否出去玩。取值為yes的記錄有9個,取值為no的有5個,即說這個樣本里有9個正例,5 個負例,記為S(9+,5-),S是樣本的意思(Sample)。那麼P(c1) = 9/14, P(c2) = 5/14
這裡熵記為Entropy(S),計算公式為:
Entropy(S)= -(9/14)*log2(9/14)-(5/14)*log2(5/14)用Matlab做數學運算
2 分別以Wind、Humidity、Outlook和Temperature作為根節點,計算其資訊增益

我們來計算Wind的資訊增益
當Wind固定為Weak時:記錄有8條,其中正例6個,負例2個;
同樣,取值為Strong的記錄6個,正例負例個3個。我們可以計算相應的熵為:
Entropy(Weak)=-(6/8)*log(6/8)-(2/8)*log(2/8)=0.811
Entropy(Strong)=-(3/6)*log(3/6)-(3/6)*log(3/6)=1.0
現在就可以計算出相應的資訊增益了:
所以,對於一個Wind屬性固定的分類系統的資訊量為 (8/14)*Entropy(Weak)+(6/14)*Entropy(Strong)
Gain(Wind)=Entropy(S)-(8/14)*Entropy(Weak)-(6/14)*Entropy(Strong)=0.940-(8/14)*0.811-(6/14)*1.0=0.048
這個公式的奧祕在於,8/14是屬性Wind取值為Weak的個數佔總記錄的比例,同樣6/14是其取值為Strong的記錄個數與總記錄數之比。

同理,如果以Humidity作為根節點:
Entropy(High)=0.985 ; Entropy(Normal)=0.592
Gain(Humidity)=0.940-(7/14)*Entropy(High)-(7/14)*Entropy(Normal)=0.151
以Outlook作為根節點:
Entropy(Sunny)=0.971 ; Entropy(Overcast)=0.0 ; Entropy(Rain)=0.971
Gain(Outlook)=0.940-(5/14)*Entropy(Sunny)-(4/14)*Entropy(Overcast)-(5/14)*Entropy(Rain)=0.247
以Temperature作為根節點:
Entropy(Cool)=0.811 ; Entropy(Hot)=1.0 ; Entropy(Mild)=0.918
Gain(Temperature)=0.940-(4/14)*Entropy(Cool)-(4/14)*Entropy(Hot)-(6/14)*Entropy(Mild)=0.029
這樣我們就得到了以上四個屬性相應的資訊增益值:
Gain(Wind)=0.048 ;Gain(Humidity)=0.151 ; Gain(Outlook)=0.247 ;Gain(Temperature)=0.029
最後按照資訊增益最大的原則選Outlook為根節點。子節點重複上面的步驟。這顆樹可以是這樣的,它讀起來就跟你認為的那樣:
下面是《機器學習實戰》的原始碼:
<span style="font-size:18px;">'''
Created on Oct 12, 2010
Decision Tree Source Code for Machine Learning in Action Ch. 3
@author: Peter Harrington
'''
from math import log
import operator

def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing','flippers']
    #change to discrete values
    return dataSet, labels

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet: #the the number of unique elements and their occurance
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * log(prob,2) #log base 2
    return shannonEnt
    
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]     #chop out axis used for splitting
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet
    
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #the last column is used for the labels
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures):        #iterate over all the features
        featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
        uniqueVals = set(featList)       #get a set of unique values
        newEntropy = 0.0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)     
        infoGain = baseEntropy - newEntropy     #calculate the info gain; ie reduction in entropy
        if (infoGain > bestInfoGain):       #compare this to the best gain so far
            bestInfoGain = infoGain         #if better than current best, set to best
            bestFeature = i
    return bestFeature                      #returns an integer

def majorityCnt(classList):
    classCount={}
    for vote in classList:
        if vote not in classCount.keys(): classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]
    if classList.count(classList[0]) == len(classList): 
        return classList[0]#stop splitting when all of the classes are equal
    if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]       #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree                            
    
def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'w')
    pickle.dump(inputTree,fw)
    fw.close()
    
def grabTree(filename):
    import pickle
    fr = open(filename)
    return pickle.load(fr)
    
</span>

生成圖片的:

<span style="font-size:18px;">'''
Created on Oct 14, 2010

@author: Peter Harrington
'''
import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")

def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            numLeafs += getNumLeafs(secondDict[key])
        else:   numLeafs +=1
    return numLeafs

def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth

def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',
             xytext=centerPt, textcoords='axes fraction',
             va="center", ha="center", bbox=nodeType, arrowprops=arrow_args )
    
def plotMidText(cntrPt, parentPt, txtString):
    xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)

def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]     #the text label for this node should be this
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
    plotMidText(cntrPt, parentPt, nodeTxt)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes   
            plotTree(secondDict[key],cntrPt,str(key))        #recursion
        else:   #it's a leaf node print the leaf node
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
#if you do get a dictonary you know it's a tree, and the first element will be another dict

def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

#def createPlot():
#    fig = plt.figure(1, facecolor='white')
#    fig.clf()
#    createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
#    plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
#    plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
#    plt.show()

def retrieveTree(i):
    listOfTrees =[{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
                  {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
                  ]
    return listOfTrees[i]

#createPlot(thisTree)</span>

可以看出這個演算法只能處理離散值,這就是ID3致命的缺點,而且ID3的資訊增益會偏向value比較多的屬性,於是C.45演算法出現了

它是基於ID3演算法進行改進後的一種重要演算法,相比於ID3演算法,改進有如下幾個要點:

  • 用資訊增益率來選擇屬性。ID3選擇屬性用的是子樹的資訊增益,這裡可以用很多方法來定義資訊,ID3使用的是熵(entropy, 熵是一種不純度度量準則),也就是熵的變化值,而C4.5用的是資訊增益率。
  • 在決策樹構造過程中進行剪枝,因為某些具有很少元素的結點可能會使構造的決策樹過適應(Overfitting),如果不考慮這些結點可能會更好。
  • 對非離散資料也能處理。
  • 能夠對不完整資料進行處理。

首先,說明一下如何計算資訊增益率。
熟悉了ID3演算法後,已經知道如何計算資訊增益,計算公式如下所示(來自Wikipedia):
info-gain
或者,用另一個更加直觀容易理解的公式計算:

  • 按照類標籤對訓練資料集D的屬性集A進行劃分,得到資訊熵:

info

  • 按照屬性集A中每個屬性進行劃分,得到一組資訊熵:

infoA

  • 計算資訊增益

然後計算資訊增益,即前者對後者做差,得到屬性集合A一組資訊增益:
gain
這樣,資訊增益就計算出來了。

  • 計算資訊增益率

下面看,計算資訊增益率的公式,如下所示(來自Wikipedia):
IGR
其中,IG表示資訊增益,按照前面我們描述的過程來計算。而IV是我們現在需要計算的,它是一個用來考慮分裂資訊的度量,分裂資訊用來衡量屬性分 裂資料的廣度和均勻程式,計算公式如下所示(來自Wikipedia):
IV
簡化一下,看下面這個公式更加直觀:
H(V)
其中,V表示屬性集合A中的一個屬性的全部取值。

我們以一個很典型被引用過多次的訓練資料集D為例,來說明C4.5演算法如何計算資訊增益並選擇決策結點。

上面的訓練集有4個屬性,即屬性集合A={OUTLOOK, TEMPERATURE, HUMIDITY, WINDY};而類標籤有2個,即類標籤集合C={Yes, No},分別表示適合戶外運動和不適合戶外運動,其實是一個二分類問題。
我們已經計算過資訊增益,這裡直接列出來,如下所示:
資料集D包含14個訓練樣本,其中屬於類別“Yes”的有9個,屬於類別“No”的有5個,則計算其資訊熵:

1 Info(D) = -9/14 * log2(9/14) - 5/14 * log2(5/14) = 0.940

下面對屬性集中每個屬性分別計算資訊熵,如下所示:

1 Info(OUTLOOK) = 5/14 * [- 2/5 * log2(2/5) – 3/5 * log2(3/5)] + 4/14 * [ - 4/4 * log2(4/4) - 0/4 * log2(0/4)] + 5/14 * [ - 3/5 * log2(3/5) – 2/5 * log2(2/5)] = 0.694
2 Info(TEMPERATURE) = 4/14 * [- 2/4 * log2(2/4) – 2/4 * log2(2/4)] + 6/14 * [ - 4/6 * log2(4/6) - 2/6 * log2(2/6)] + 4/14 * [ - 3/4 * log2(3/4) – 1/4 * log2(1/4)] = 0.911
3 Info(HUMIDITY) = 7/14 * [- 3/7 * log2(3/7) – 4/7 * log2(4/7)] + 7/14 * [ - 6/7 * log2(6/7) - 1/7 * log2(1/7)] = 0.789
4 Info(WINDY) = 6/14 * [- 3/6 * log2(3/6) – 3/6 * log2(3/6)] + 8/14 * [ - 6/8 * log2(6/8) - 2/8 * log2(2/8)] = 0.892

根據上面的資料,我們可以計算選擇第一個根結點所依賴的資訊增益值,計算如下所示:

1 Gain(OUTLOOK) = Info(D) - Info(OUTLOOK) = 0.940 - 0.694 = 0.246
2 Gain(TEMPERATURE) = Info(D) - Info(TEMPERATURE) = 0.940 - 0.911 = 0.029
3 Gain(HUMIDITY) = Info(D) - Info(HUMIDITY) = 0.940 - 0.789 = 0.151
4 Gain(WINDY) = Info(D) - Info(WINDY) = 0.940 - 0.892 = 0.048

接下來,我們計算分裂資訊度量H(V):

  • OUTLOOK屬性

屬性OUTLOOK有3個取值,其中Sunny有5個樣本、Rainy有5個樣本、Overcast有4個樣本,則

1 H(OUTLOOK) = - 5/14 * log2(5/14) - 5/14 * log2(5/14) - 4/14 * log2(4/14) = 1.577406282852345
  • TEMPERATURE屬性

屬性TEMPERATURE有3個取值,其中Hot有4個樣本、Mild有6個樣本、Cool有4個樣本,則

1 H(TEMPERATURE) = - 4/14 * log2(4/14) - 6/14 * log2(6/14) - 4/14 * log2(4/14) = 1.5566567074628228
  • HUMIDITY屬性

屬性HUMIDITY有2個取值,其中Normal有7個樣本、High有7個樣本,則

1 H(HUMIDITY) = - 7/14 * log2(7/14) - 7/14 * log2(7/14) = 1.0
  • WINDY屬性

屬性WINDY有2個取值,其中True有6個樣本、False有8個樣本,則

1 H(WINDY) = - 6/14 * log2(6/14) - 8/14 * log2(8/14) = 0.9852281360342516

根據上面計算結果,我們可以計算資訊增益率,如下所示:

1 IGR(OUTLOOK) = Info(OUTLOOK) / H(OUTLOOK) = 0.246/1.577406282852345 = 0.15595221261270145
2 IGR(TEMPERATURE) = Info(TEMPERATURE) / H(TEMPERATURE) = 0.029 / 1.5566567074628228 = 0.018629669509642094
3 IGR(HUMIDITY) = Info(HUMIDITY) / H(HUMIDITY) = 0.151/1.0 = 0.151
4 IGR(WINDY) = Info(WINDY) / H(WINDY) = 0.048/0.9852281360342516 = 0.048719680492692784

根據計算得到的資訊增益率進行選擇屬性集中的屬性作為決策樹結點,對該結點進行分裂。

C4.5演算法的優點是:產生的分類規則易於理解,準確率較高。
C4.5演算法的缺點是:在構造樹的過程中,需要對資料集進行多次的順序掃描和排序,因而導致演算法的低效。

C.45的演算法

image

我自己用python實現了一下C.45

首先在UCI找資料http://archive.ics.uci.edu/ml/datasets.html

看中了這個http://archive.ics.uci.edu/ml/datasets/Thyroid+Disease

Data Set Information:

# From Garavan Institute 
# Documentation: as given by Ross Quinlan 
# 6 databases from the Garavan Institute in Sydney, Australia 
# Approximately the following for each database: 

嘿,還是C.45作者提供的資料呢,就選它了。

下了alldp.data和alldp.test

資料的文件頭部需要自己新增header,把.|替換成,

<span style="font-size:18px;">__author__ = 'tyan'
#coding=utf-8
import os
import math
import copy
import operator
from collections import Counter

class Node:
    def __init__(self, val, child=[], condition=None):
        self.val = val
        self.child = child
        self.condition = condition

class C4_5(object):

    #初始化
    def __init__(self, trainSet, format, rule):
        self.tree = Node(None, [])
        trainSet = list(trainSet)
        self.attributes = trainSet[0][:-1]
        self.format = format
        self.trainSet = trainSet[1:]
        self.dataLen = len(self.trainSet)
        self.rule = rule

    def startTrain(self):
        self.train(self.trainSet, self.tree, self.attributes, 0)

    #處理缺失值,我這是圖方便,實際測試predict時,不建議用測試樣本中的資料來生成缺失資料,應該用訓練資料來生成
    def rep_miss(self, dataSet):
        exp = copy.deepcopy(dataSet)
        for attr in self.attributes:
            idx = self.attributes.index(attr)
            if self.format[idx] == 'nominal':
                #expN 用頻率最大的填補缺失
                expN = getDefault([item[idx] for item in exp])
                for item in exp:
                    if item[idx] == '?':
                        item[idx] = expN
                else:
                    num_lst  = [float(item[idx]) for item in exp if item[idx] != '?']
                    mean = sum(num_lst) / len(num_lst)
                    for item in exp:
                        if item[idx] == '?':
                            item[idx] = mean
        return exp

    #尋找合適的分割點
    def split(self, lst, idx):
        split_candidate = []
        for x, y in zip (lst, lst[1:]):
            if (x[-1] != y[-1]) and (x[idx] != y[idx]):
                split_candidate.append( (x[idx] + y[idx]) / 2 )
        return split_candidate

    def preProcess(self, validFile):
        validSet = list( readData(validFile) )
        validData = validSet[1:]
        exp = self.rep_miss(validData)
        return exp

    def rule_generator(self, tree, single_rule):
        #if flag:
        #	self.rule = []
        #print tree
        if tree.child:
            if isinstance(tree.val, list):
                single_rule.append(tree.val)
            if tree.child[0] == 'negative':
                single_rule.append(['class', '=', 'negative'])
                self.rule.append(single_rule)
            elif tree.child[0] == 'increased binding proteinval':
                single_rule.append(['class', '=', 'increased binding proteinval'])
                self.rule.append(single_rule)
            elif tree.child[0] == 'decreased binding protein':
                single_rule.append(['class', '=', 'decreased binding protein'])
                self.rule.append(single_rule)
            else:
                for item in tree.child:
                    self.rule_generator(item, list(single_rule))

    def train(self, dataSet, tree, attributes, default):
        if len(dataSet) == 0:
            return Node(default)
        elif allequal([item[-1] for item in dataSet]):
            return Node(dataSet[0][-1])
        elif len(attributes) == 0:
            return Node(getDefault([item[-1] for item in dataSet]))
        else:
            #選取最大資訊增益
            best = self.choose_attr(attributes, dataSet)
            if best == 0:
                return Node(getDefault([item[-1] for item in dataSet]))
            print best
            tree.val = best[0]
            #離散值的情況
            idx = self.attributes.index(best[0])
            if best[1] == 'nom':
                attributes.remove(best[0])
                for v in unique(item[idx] for item in dataSet):
                    subDataSet = [item for item in dataSet if item[idx] == v]
                    #選取條件熵後的子資料集遞迴構造樹
                    subTree = self.train(subDataSet, Node(None, []), list(attributes), getDefault(item[-1] for item in dataSet))
                    branch = Node([best[0], '==', v], [subTree])
                    tree.child.append(branch)
            else:#連續型變數
                subDataSet1 = [item for item in dataSet if float(item[idx]) > best[2]]
                default = getDefault(item[-1] for item in dataSet)
                if len(subDataSet1) == len(dataSet):
                    print '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'
                    return default
                subTree1 = self.train(subDataSet1, Node(None), list(attributes), default)
                subTree1.condition = [best[0], '>', str(best[2])]
                tree.child.append(subTree1)

                subDataSet2 = [item for item in dataSet if float(item[idx]) <= best[2]]
                subTree2 = self.train(subDataSet2, Node(None), list(attributes), default)
                subTree2.condition=[best[0], '<=', str(best[2])]
                tree.child.append(subTree2)
            return tree

    #求最大資訊增益比
    def choose_attr(self, attributes, dataSet):
        maxIGR = 0.0
        dataLen = float(len(dataSet))
        group = [item[-1] for item in dataSet]
        groupC = Counter(group).items()
        #sysGI 分類系統熵
        sysGI = entropy([vl/dataLen for k,vl in groupC])
        for attr in attributes:
            idx = self.attributes.index(attr)
            gain = sysGI
            h = 0.0 #資訊裂度
            if self.format[idx] == 'nominal':
                #expN 把頻率最大的填補缺失
                expN = getDefault([item[idx] for item in dataSet])
                for item in dataSet:
                    if item[idx] == '?':
                        item[idx] = expN
                for i in unique([item[idx] for item in dataSet]):
                    #expG:該attr的所有分類結果
                    expG = [item[-1] for item in dataSet if item[idx] == i]
                    expGC = Counter(expG).items()
                    split_len = float(len(expG))
                    gain -= split_len/dataLen * entropy([vl/split_len for k,vl in expGC])
                #計算資訊裂度
                groupValueC = Counter([item[idx] for item in dataSet ]).items()
                h -=  entropy([vl/len(dataSet) for k,vl in groupValueC])
                if h == 0:
                    continue #不知道為什麼會有0,鬱悶
                igr = gain / h
                if igr > maxIGR:
                    maxIGR = gain
                    best = [attr, 'nom']
            else:
                num_lst = [float(item[idx]) for item in dataSet if item[idx] != '?']
                if len(num_lst) == 0:
                    print "Error!!!!"
                mean = sum(num_lst) / len(num_lst)
                exps = list(dataSet)
                for item in exps:
                    if item[idx] == '?':
                        item[idx] = mean
                    else:
                        item[idx] = float(item[idx])
                exps.sort(key = operator.itemgetter(idx))
                split_candidate = self.split(exps, idx)
                for thresh in split_candidate:
                    gain = sysGI
                     #expG:該attr的所有分類結果
                    expG1 = [item[-1] for item in exps if float(item[idx]) > thresh]
                    expG2 = [item[-1] for item in exps if float(item[idx]) <= thresh]
                    len1 = float(len(expG1))
                    len2 = float(len(expG2))
                    if len1 == 0 or len2 == 0:
                        gain = 0
                    else:
                        expGC1 = Counter(expG1).items()
                        expGC2 = Counter(expG2).items()
                        gain -= len1/dataLen * entropy([vl/len1 for k,vl in expGC1])
                        gain -= len1/dataLen * entropy([vl/len1 for k,vl in expGC1])
                    h -= entropy([len1/len(dataSet), len2/len(dataSet)])
                    igr = gain / h
                    if igr > maxIGR:
                        maxIGR = igr
                        best = [attr, 'num', thresh]
        #print max_gain
        if maxIGR <= 0:
            return 0
        return best

def entropy(lst):
    entrop = 0.0
    for p in lst:
        if p == 0:
            continue
        entrop -= p * math.log(p, 2)
    return entrop

def unique(seq):
    keys = {}
    for e in seq:
        keys[e] = 1
    return keys.keys()

def allequal(seq):
    flag = seq[0]
    for item in seq:
        if item != flag:
            return 0
    return 1

def readData(inputfile):
    data = []
    abspath = os.path.abspath(inputfile)
    with open(abspath,"r")as file:
        text = file.readlines()
    for line in text:
        items = line.split(',')[:-1]
        items.pop(26)
        items.pop(26)
        data.append(items)
    print data[0]
    print len(data[0])
    return data

#這個函式是選取頻率最大的
def getDefault(lst):
    frequent = Counter(lst)
    mostfrequent = frequent.most_common(2)
    if mostfrequent[0][0] == '?':
        mostfrequent = mostfrequent[1:]
    return mostfrequent[0][0]

format = []
for i in range(28):
    format.append("nominal")
for i in [0,17,19,21,23,25]:
    format[i] = "numeric"

inputfile = "allbp"
trainSet = readData(inputfile)
classifier = C4_5(trainSet, format, [])
classifier.startTrain()
</span>
跑了一下,明顯過擬合了("▔□▔)/\("▔□▔)/
剪枝和測試沒寫(其實我平時用java,現在學python學得不好,寫得爛,沒有動力寫下去了,慚愧啊 (╯#-_-)╯)

好吧,說說剪枝

過擬合是決策樹的大問題

決策樹為什麼要剪枝?原因