決策樹——Decision Tree


前言

生活中有很多利用決策樹的例子。西瓜書上給的例子是西瓜問題(講到這突然想到書中不少西瓜的例子,難道這就是它西瓜封面的由來?)\。大致意思是,已經有一堆已知好瓜壞瓜的西瓜,每次挑取西瓜的一條屬性,將西瓜進行分類。然后在分類的西瓜中,繼續挑取下一條屬性進行更加細致的划分,直到所有的屬性被用完。

這個例子有一個隱含的前提,就是給出所有的屬性,它唯一地決定西瓜的好壞。意思是,不存在兩個都是好瓜的瓜,或者都是壞瓜的瓜,它們的所有屬性都相同。這有點類似數據庫中的實體完整性。這樣,你才能通過構建的決策樹,按照它的用來分類的屬性的順序,每次前進一個分支,直到最后一層就能知道待測西瓜是否是好瓜。

但是如果由許多瓜,有好瓜也有壞瓜,他們的屬性都相同的時候,就意味着你沿決策樹到達葉子節點的時候,面臨多種選擇。這時候,可以選擇出現次數最多的瓜種作為結果返回。

(這是用手機從西瓜書上拍下來的。結果上傳的時候才發現忘記設置照片大小了,2M)

決策樹的最關鍵的問題是,如何選擇划分屬性的順序才能使得決策樹的平均性能最好(即平均在樹上走最少的路徑就可以知道結果)。比如說,如果觸感就能夠唯一決定一個瓜是否是好壞,但是觸感確實放在最后進行划分的,那么前面走的所有步驟都是多余的。如果一開始就用觸感來進行划分,那么只需要走一步,就能夠知道西瓜是否是好瓜。

下面來解釋一個概念,它能夠決定用何種屬性進行划分。

划分數據集最大的原則就是:將無序的數據變得更加有序。我們選取的是,能夠將數據划分得“最有序”的屬性。換句話說,選取能夠給出最多信息的屬性進行划分。打個比方,我要判斷一個蚊子是否是母蚊子,你首先告訴我它有翅膀。可是所有的蚊子都有翅膀,這對我進行分類毫無意義,就稱“有無翅膀”這一屬性完全沒有給出任何信息。如果你告訴我它經常在你身邊上下騰飛騷擾你,那么我能夠給出八成的可能說它是母蚊子,就說它給出了部分信息。你如果告訴我它會吸血,那母蚊子沒跑了。“會不會吸血”這一屬性給出了決定性的信息。

熵(Entropy)定義為信息的期望值,它在物理學上表示物質的混亂程度。在信息學上可以作類比。熵增加,表示信息更加混亂,熵減,表示信息更加有序。假設划分之前,這堆西瓜的熵是$Ent(D)$,按照某種屬性划分之后這堆西瓜的熵是$Ent(D')$,注意后者一定小於等於前者。否則划分就沒有任何意義。這兩者的差,就是這個屬性對於這堆西瓜有序化的貢獻。下面說明熵的計算方法:

假設樣本集$D$中,第$k$類樣本所占的比例為$p_k$,那么$D$的熵:

$Ent(D) = - \Sigma p_klog_2p_k$

這里約定$p_k = 0$時,$p_klog_2p_k  = 0$。當這樣本集被某個屬性划分成$v$份之后,熵就是所有子集的熵的和:

$Ent(D') = -\Sigma Ent(D^i) \ \  ,i = [1,...,v]$

但是,每個樣本子集中划分到的樣本數量不一樣,需要對每個子集的熵進行加權:

$Ent(D') = -\Sigma \frac{|D^i|}{|D|}Ent(D^i) \ \  ,i = [1,...,v]$

於是熵增就定義為

$Gain(D,a) = Ent(D) - Ent(D') = Ent(D) - \Sigma \frac{|D^i|}{|D|}Ent(D^i) \ \ ,i = [1,...,v]$

表示,按照屬性a對D進行划分熵的增益。顯然,增益越大,表示越應該提前使用該屬性進行划分。

下面給出計算熵增的python2.x 代碼

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet:
        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)
    return shannonEnt

這里的dataSet,由多個向量組成,每個向量代表一個樣本。這里的向量的最后一個元素代表該樣本的類別。這不同於之前的KNN算法。KNN的類別和屬性是分開表示的。

第一個for循環計算每個類別的概率。第二個for循環計算熵。

決策樹完整算法

輸入:樣本集D,每個樣本包含他們的屬性(值)和類別。

輸出:決策樹

算法描述:

  1. 選擇一個最好的划分屬性
  2. 用最好的屬性划分D,並從屬性列表中刪除該屬性。
  3. 對每個子集,如果屬性已經用完,返回類別標簽。否則對每個子集,重復1步驟。

python2.x 代碼實現

# 使用坐標為axis的屬性划分dataSet,將該屬性的值等於value的樣本返回。並且從樣本中刪除該屬性
def splitDataSet(dataSet,axis,value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

# 選擇最好的划分屬性,它返回的是屬性在屬性向量中的位置(坐標)
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0
    bestFeature = -1
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)
        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
        if(infoGain>bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

# dataSet 中的向量包含了屬性和類別
def createTree(dataSet,labels):
    # 取所有的類別
    classList = [example[-1] for example in dataSet]
    # 如果所有的類別都相同,就返回
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果dataSet中已經沒有屬性了,表示所有的屬性已經被遍歷完,就返回出現次數最多的屬性
    if len(dataSet[0] == 1):
        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[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
    return myTree

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]

這里的決策樹用的是python的字典嵌套來表示。字典每一層的key由屬性名和屬性值交替,如果這一層的key是屬性名,表示由該屬性名進行划分,下一層的字典就是該屬性的所有可能的取值。然后每一個對於每一個取值,再選一個屬性名。如果沒有屬性名可選,最終的字典就是類別。具體的含義可以參考這篇博客:python中字典{}的嵌套。他里面用的例子剛好是這篇機器學習的代碼。

最后一個函數是為了解決葉子節點有多種結果情況。選擇出現次數最多的結果返回。函數createTree中第二個參數labels是屬性的名稱,一般類型是字符串,比如‘色澤’,‘外觀’,‘觸感’之類的。而第一個參數dataSet中的屬性是屬性的值。

其他

上述代碼中構造的決策樹是一個嵌套字典。具體如何使用,其實就是下標索引,在上面給出的博客中也有講。

決策樹的構建過程耗時比較長,但是構建好之后測試樣本就很快。他不同於上一篇的KNN,KNN每次需要測試樣本的時候,都需要重新訓練一次,即‘懶惰學習’。決策樹只有在拿到新的訓練樣本的時候才需要訓練,以后需要測試樣本都可以調用已經構建好的決策樹,即‘急切學習’。關於構建出來的用嵌套字典表示的決策樹如何繪制出來和存儲,在《機器學習實戰》里面也有詳細提到,分別用matplotlib和pickle庫。因為我這兩個都不會用,python也是先學不到兩周,還需要學習,所以就不搬運了。如果什么時候覺得這非常需要作篇博客以表學習,那到時就另起一篇,放在python分類中。

增益率

西瓜書還提到這個概念。

是想,如果把訓練樣本的每一個樣本都作編號,然后把編號也作為屬性參與決策。通過計算,編號產生的信息熵增益達到0.998,遠高於其他屬性。這很好理解:每一個編號都產生一個分支,每個分支僅僅包含一個樣本,這樣就完全不需要考慮其他屬性了。然而,這樣的決策樹並不具有泛化能力,無法對新樣本做出有效預測。

信息增益准則對取值數目多的屬性有偏好。意思是,一個屬性的取值越多,越有可能被選為當前最好的划分屬性。為了解決這種偏好帶來的不利映像,著名的C4.5決策樹算法不直接使用信息增益,而是使用“增益率”來選擇最優划分屬性。增益率定義為:
$Gain\_ratio(D,a) = \frac{Gain(D,a)}{IV(a)}$

其中

$IV(a) = -\Sigma \frac{|D^v|}{|D|}log_2\frac{|D^v|}{|D|}$

$IV(a)$稱為屬性a的“固有值”。屬性a的可能取值越多,固有值越大,增益率越小。可以看到,原本的信息增益使用屬性的固有值進行了加權,消除了原本算法對取值數目多的屬性的偏好帶來的影響。

西瓜書上還有一段:

需要注意的是,增益率准則對可取值數目較少的屬性有所偏好,因此,C4.5算法並不是直接選擇增益率最大的候選划分屬性,而是使用了一個啟發式:先從候選划分屬性中找出信息增益高於平均水平的屬性,再從中選擇增益率最高的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM