機器學習之決策樹ID3(python實現)

swensun發表於2019-02-27

機器學習中,決策樹是一個預測模型;代表物件屬性和物件值之間的一種對映關係。樹中每個節點表示某個物件,而每個分叉表示某個可能的屬性,每個葉子節點則對應從根節點到該葉子節點所經歷的路徑所表示的物件的值。決策樹只有單一輸出,若想要複數輸出,可以建立獨立的決策樹以處理不同輸入。
資料探勘中常用到決策樹,可以用於分析資料,也可以用於預測。

簡單理解

image.png

如上圖,
前兩個是屬性,可以記為[`no surfacing`,`flippers`]。則可以簡單的構建決策樹如下:

image.png

根據兩個屬性可以判斷是否屬於魚類。

那麼首先決定選擇哪個屬性作為最開始的分類?最簡單的是ID3。改進的C4.5, CART後面再進行了解。

決策樹和ID3

決策樹與樹結構類似,具有樹形結構。每個內部節點表示一個屬性的測試,每個分支代表一個測試輸出,每個葉子節點代表一種類別。如上圖一樣。
分類樹(決策樹)常用於機器學習的分類,是一種監督學習方法。由樹的分支對該型別的物件依靠屬性進行分類。每個決策樹可以依靠對源資料庫分割進行資料測試,遞迴修剪樹。知道一個單獨的類被應用於某一分支,不能進行分割,遞迴完成。
特點:

  • 多層次的決策樹形式易於理解。
  • 只適用於標稱行資料,連續性資料處理的不好。

ID3演算法

上面介紹,怎麼在一系列屬性中首先選擇哪個屬性進行分類。簡單理解,如果哪個屬性比較混亂,直接就可以得到所屬類別。比如上面屬性水下是否可以生存,不能生存的可以分類為 不是魚。
那麼怎麼去量化,得到這個屬性呢?
ID3演算法的核心是資訊墒,通過計算每個屬性的資訊增益,認為增益高的是好屬性,易於分類。每次劃分選取資訊增益最高的屬性作為劃分標準,進行重複,直至生成一個能完美分類訓練樣歷的決策樹。

image.png

上面資訊增益的演算法不是很好理解,後面看程式碼很容易。

ID3演算法和決策樹的流程

  1. 資料準備:需要對數值型資料進行離散化
  2. ID3演算法構建決策樹:
  • 如果資料類別完全相同,則停止劃分。
  • 否則,繼續劃分:
    • 計算資訊墒和資訊增益來選擇最好的資料集劃分方法
    • 劃分資料集
    • 建立分支節點
    • 對每個分支進行判定類別相同。相同停止劃分,不同則按照上述方法進行劃分。

python程式碼實現

利用上面例子建立資料集

def createDataSet():
    dataSet = [[1, 1, `yes`], [1, 1, `yes`], [1, 0, `no`], [0, 1, `no`], [0, 1, `no`]]
    labels = [`no sufacing`, `flippers`]
    return dataSet, labels
複製程式碼

計算資訊墒,對應第一個公式

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    # 為分類建立字典
    labelCounts = {}
    for featVec in dataSet:
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts.setdefault(currentLabel, 0)
        labelCounts[currentLabel] += 1

    # 計算夏農墒
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key]) / numEntries
        shannonEnt += prob * math.log2(1 / prob)
    return shannonEnt
複製程式碼

計算最大資訊增益(公式2), 劃分資料集。

# 定義按照某個特徵進行劃分的函式 splitDataSet
# 輸入三個變數(帶劃分資料集, 特徵,分類值)
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reduceFeatVec = featVec[:axis]
            reduceFeatVec.extend(featVec[axis + 1:])
            retDataSet.append(reduceFeatVec)
    return retDataSet  #返回不含劃分特徵的子集

#  定義按照最大資訊增益劃分資料的函式
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1
    print(numFeature)
    baseEntropy = calcShannonEnt(dataSet)
    bestInforGain = 0
    bestFeature = -1

    for i in range(numFeature):
        featList = [number[i] for number in dataSet] #得到某個特徵下所有值
        uniqualVals = set(featList) #set無重複的屬性特徵值
        newEntrogy = 0

        #求和
        for value in uniqualVals:
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet) / float(len(dataSet)) #即p(t)
            newEntrogy += prob * calcShannonEnt(subDataSet) #對各子集求夏農墒

        infoGain = baseEntropy - newEntrogy #計算資訊增益
        print(infoGain)

        # 最大資訊增益
        if infoGain > bestInforGain:
            bestInforGain = infoGain
            bestFeature = i
    return bestFeature
複製程式碼

簡單測試:

if __name__ == `__main__`:
    dataSet, labels = createDataSet()
    r = chooseBestFeatureToSplit(dataSet)
    print(r)
# 輸出
# 2
# 0.41997309402197514
# 0.17095059445466865
# 0
複製程式碼

如上,可以看到共有兩個屬性[`no surfacing`,`flippers`]和其資訊增益,因此選擇較大的特徵(下標0)對資料集進行劃分(見開始圖),重複步驟,知道只剩下一個類別。

建立決策樹建構函式

# 投票表決程式碼
def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount.setdefault(vote, 0)
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.items(), key=lambda i:i[1], reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # print(dataSet)
    # print(classList)
    # 類別相同,停止劃分
    if classList.count(classList[0]) == len(classList):
        return classList[0]

    # 判斷是否遍歷完所有的特徵,是,返回個數最多的類別
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    #按照資訊增益最高選擇分類特徵屬性
    bestFeat = chooseBestFeatureToSplit(dataSet) #分類編號
    bestFeatLabel = labels[bestFeat]  #該特徵的label
    myTree = {bestFeatLabel: {}}
    del (labels[bestFeat]) #移除該label

    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
複製程式碼

程式碼裡面有注視,嘗試去理解每一步的執行,對決策樹有一個基本的瞭解。

if __name__ == `__main__`:
    dataSet, labels = createDataSet()
    r = chooseBestFeatureToSplit(dataSet)
    # print(r)
    myTree = createTree(dataSet, labels)
    print(myTree)
#  --> {`no sufacing`: {0: `no`, 1: {`flippers`: {0: `no`, 1: `yes`}}}}
複製程式碼

可以看到輸出結果是一個巢狀的字典,手動可以畫出決策樹,與開頭的圖相吻合。

將決策樹用於分類

構建決策樹分類函式:

def classify(inputTree, featLabels, testVec):
    """
    :param inputTree: 決策樹
    :param featLabels: 屬性特徵標籤
    :param testVec: 測試資料
    :return: 所屬分類
    """
    firstStr = list(inputTree.keys())[0] #樹的第一個屬性
    sendDict = inputTree[firstStr]

    featIndex = featLabels.index(firstStr)
    classLabel = None
    for key in sendDict.keys():

        if testVec[featIndex] == key:
            if type(sendDict[key]).__name__ == `dict`:
                classLabel = classify(sendDict[key], featLabels, testVec)
            else:
                classLabel = sendDict[key]
    return classLabel
複製程式碼

可以看到函式分別根據屬性值對測試資料進行一步一步分類,直至到葉子節點,得到正確的分類。

另外,可以將決策樹進行儲存,與kNN不一樣的是,決策樹構造好不用重複計算,下次可以直接使用.

def storeTree(inputTree,filename):
    import pickle
    fw=open(filename,`wb`) #pickle預設方式是二進位制,需要制定`wb`
    pickle.dump(inputTree,fw)
    fw.close()

def grabTree(filename):
    import pickle
    fr=open(filename,`rb`)#需要制定`rb`,以byte形式讀取
    return pickle.load(fr)
複製程式碼

完整決策樹程式碼請檢視github:
github:decision_tree

總結

  • 決策樹: ID3, C4.5, CART
  • 資訊理論:資訊墒,資訊增益
  • python物件儲存

參考資料:
機器學習之決策樹(ID3)演算法與Python實現

相關文章