1. 程式人生 > >決策樹分析與實踐

決策樹分析與實踐

1.分析

1.1 背景和意義:

       相信很多人都玩過一個網路上傳的遊戲,腦海裡面想一個名人的名字,然後出若干多道問題,比如男的女的,國外的國內的,你只能答是或不是,最後給出你想的那個名人是誰。只要不是很偏的應該都能想出來,一般人覺得很震驚,其實這只是一種簡單機器演算法——決策樹的應用而已。         決策樹簡單易懂,符合人的處理問題的方式,用在專家系統中甚至可以匹敵在當前領域具有十幾年工作經驗的專家。可以幫助我們解決分類與迴歸兩類問題。本文講解的是分類決策樹。         在如今的機器學習領域中,樹模型可以說是最為重要的模型,在各種資料探勘的競賽中和生產環境中,威力巨大的大殺器XGboost和lightGBM 兩個工具都是基於樹模型構建的。學習決策樹,要掌握兩個關鍵問題:第一,面對一個實際的資料集,應該如何構建出一顆樹;第二,在構建樹的過程中,樹分裂節點時,如何選擇出最優的屬性作為分裂節點。

1.2 定義:

        分類決策樹是一種對例項進行分類的樹形結構。由結點和有向邊組成。結點有兩種型別:內結點(表示一個特徵),葉節點(表示一個類別)。

  •  怎麼應用決策樹來進行分類呢?

         首先,從根結點開始,對例項的當前分類的特徵進行測試得到它屬於該特徵的哪一個取值分支,然後分配到該特徵取值子節點(每個子結點對應該特徵的一個取值),對子結點做同樣的事,遞迴地對例項進行測試分配,直到到達葉結點,就分配到對應的類別中了。

  • 為什麼這樣可以進行分類?

        決策樹可以看成是一個if-then規則的集合,又或看成一個條件概率分佈。由決策樹的根結點到葉結點的每一條路構建起一條分類規則:路徑上的內結點的特徵對應該規則的一個條件,葉結點的類別對應按照該規則分類的結果。決策樹的路徑是互斥且完備的,就是每一個例項進入該樹最終只能通過一條路徑到達唯一的結果。

1.3 決策樹分類演算法的原理

  • 面對一個實際的資料集,應該如何構建出一顆決策分類樹;

概述:遞迴地選擇當前最優特徵,並根據該特徵對訓練資料集進行分割,使得對各個子資料資料集都有一個最好的分類的過程,這個過程在劃分特徵空間的同時也構建了決策樹。

具體:開始構建根結點,將所有的資料都先放在根結點,選擇一個最優特徵來分割訓練資料整合子集。如果子集已經基本正確分類(屬於同一類),就構建葉結點把子集放入其中;如果有些子集不能基本分類,就對其選擇一個最優特徵繼續分割……如此遞迴,直到所有訓練資料子集都被基本分類、不再有合適的分類特徵為止。這樣每一個子集都分到葉結點上,分出了對應的類別,同時也構建了決策樹。

  • 遇到的問題:

過擬合問題:要剪枝,使樹變簡單有泛化能力:就是去掉過於細分的葉結點,直接歸入父結點或者更高結點成為新的葉結點。特徵過多:要選擇有足夠分類能力的特徵。

         所以決策樹學習演算法包含特徵選擇,決策樹的生成,決策樹的剪枝         模型通過區域性最優生成決策樹(在當前結點做決定),但剪枝要考慮全域性最優(看哪些才是細分的節點)          常用的有三種演算法:ID3、C4.5與CART。CART可以用來分類也可以用來回歸,在後面的部落格中會繼續講到。

  • 那麼在構建樹的過程中,樹分裂節點時,如何選擇出最優的屬性作為分裂節點?  我們可以用資訊增益或者資訊增益比來進行決策樹的劃分屬性選擇。

資訊增益:表示得知特徵X的資訊而使得類Y的資訊的不確定性減少的程度,公式如下:

   g(D,A)=H(D)-H(D|A)

 ID3 決策樹學習演算法就是以資訊增益為準則來選擇劃分屬性。

問題:資訊增益準則對可取值數目較多的屬性有所偏好,就好比,我們獲取的樣本集資料中包含了一條屬性是每個樣本的 ID 號,那麼根據資訊增益的計算公式可以得到,當使用 ID 號作為劃分屬性時,每個分支肯定都是一個類別,它的資訊增益率極高,但是不具備泛化能力,無法對新樣本進行有效預測。

為了解決這個問題,就用到了資訊增益比。

資訊增益比:資訊增益與訓練集D關於特徵A的值的熵之比,公式如下:

g_R(D,A)=\frac{g(D,A)}{H_A(D)}

C4.5決策樹學習演算法就是以資訊增益為準則來選擇劃分屬性。

2.實踐(《機器學習實戰》程式碼解析)

3-1 計算夏農熵

from math import log
import operator

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

3-2 劃分資料集

def splitDataSet(dataSet, axis, value):#樣本資料,要選取的特徵索引(第幾個特徵),特徵的某一取值
    retDataSet = []                 # 初始化分割後的樣本集為空
    for featVec in dataSet:         # 逐行讀取樣本
        if featVec[axis] == value:  # 奇妙:把第axis個特徵等於value的除掉該特徵項後輸出,就是把左邊和右邊的合併,就等於除掉自己
            reducedFeatVec = featVec[:axis]     # 取出特徵列左側的資料
            reducedFeatVec.extend(featVec[axis+1:])  # 合併特徵列右側的資料
            retDataSet.append(reducedFeatVec)
    return retDataSet

3-3 選擇最好的分割特徵來分割資料集:根據最大資訊增益

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      # 特徵數量
    baseEntropy = calcShannonEnt(dataSet)  # 基礎熵為樣本集本身的夏農熵
    bestInfoGain = 0.0; bestFeature = -1   # 初始化最大資訊增益為0;最佳特徵索引為-1
    for i in range(numFeatures):           # 遍歷所有特徵
        featList = [example[i] for example in dataSet] # 取出第i個特徵列資料
        uniqueVals = set(featList)         # 第i個特徵列的取值集合(刪除重複元素)
        newEntropy = 0.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   

從類別列表中計算出現次數最多的類別(多數表決法)用過很多次了

def majorityCnt(classList):
    classCount = {}
    for vote in classList:    #統計classList中每個元素出現的個數
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1), reverse=True)
    print(sortedClassCount)
    return sortedClassCount[0][0]   #返回classList中出現次數最多的元素

3-4 構建決策樹

def createTree(dataSet,labels):
    # labels為每個特徵的名稱/說明
    classList = [example[-1] for example in dataSet]    # 取出類別列
    if classList.count(classList[0]) == len(classList): # 只有1個類別時停止分割,返回當前類別
        return classList[0]  
    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 classify(inputTree, featLabels, testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]).__name__=='dict':##判斷secondDict[key]是不是字典
                classLabel = classify(secondDict[key],featLabels,testVec)
            else:   classLabel = secondDict[key]
    return classLabel

使用演算法:決策樹的儲存

可以把分類器儲存在硬碟上不用每次對資料分類都要重新學習一下,這也是決策樹的優點,如KNN就無法持久化分類器。

#使用pickle模組儲存決策樹
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)