1. 程式人生 > >一步步教你輕鬆學決策樹演算法

一步步教你輕鬆學決策樹演算法

(白寧超 2018年8月27日16:28:53)

導讀:決策樹演算法是一種基本的分類與迴歸方法,是最經常使用的演算法之一。決策樹模型呈樹形結構,在分類問題中,表示基於特徵對例項進行分類的過程。它可以認為是基於規則的集合。本文首先介紹決策樹定義、工作原理、演算法流程、優缺點等,然後結合案例進行分析。(本文原創,轉載必須註明出處: 一步步教你輕鬆學決策樹

目錄

理論介紹

什麼是決策樹

  • 維基百科:決策樹(Decision Tree)是一個預測模型;他代表的是物件屬性與物件值之間的一種對映關係。樹中每個節點表示某個物件,而每個分叉路徑則代表某個可能的屬性值,而每個葉節點則對應從根節點到該葉節點所經歷的路徑所表示的物件的值。資料探勘中決策樹是一種經常要用到的技術,可以用於分析資料,同樣也可以用來作預測。從資料產生決策樹的機器學習技術叫做決策樹學習,通俗說就是決策樹。

  • 分類決策樹模型是一種描述對例項進行分類的樹形結構。決策樹由結點(node)和有向邊(directed edge)組成。結點有兩種型別:內部結點(internal node)和葉結點(leaf node)。內部結點表示一個特徵或屬性(features),葉結點表示一個類(labels)。

用決策樹對需要測試的例項進行分類:從根節點開始,對例項的某一特徵進行測試,根據測試結果,將例項分配到其子結點;這時,每一個子結點對應著該特徵的一個取值。如此遞迴地對例項進行測試並分配,直至達到葉結點。最後將例項分配到葉結點的類中。

什麼是資訊熵和資訊增益

  • 熵(entropy): 熵指的是體系的混亂的程度,在不同的學科中也有引申出的更為具體的定義,是各領域十分重要的參量。

  • 資訊理論(information theory)中的熵(夏農熵): 是一種資訊的度量方式,表示資訊的混亂程度,也就是說:資訊越有序,資訊熵越低。例如:火柴有序放在火柴盒裡,熵值很低,相反,熵值很高。

  • 資訊增益(information gain): 在劃分資料集前後資訊發生的變化稱為資訊增益,資訊增益越大,確定性越強。

決策樹工作原理

'''
決策樹工作原理:基於迭代的思想。
'''
def createBranch():
    檢測資料集中的所有資料的分類標籤是否相同:
        If so return 類標籤
        Else:
            尋找劃分資料集的最好特徵(劃分之後資訊熵最小,也就是資訊增益最大的特徵)
            劃分資料集
            建立分支節點
                for 每個劃分的子集
                    呼叫函式 createBranch (建立分支的函式)並增加返回結果到分支節點中
            return 分支節點

決策樹演算法流程

收集資料:可以使用任何方法。
準備資料:樹構造演算法 (這裡使用的是ID3演算法,只適用於標稱型資料,這就是為什麼數值型資料必須離散化。 還有其他的樹構造演算法,比如CART)
分析資料:可以使用任何方法,構造樹完成之後,我們應該檢查圖形是否符合預期。
訓練演算法:構造樹的資料結構。
測試演算法:使用訓練好的樹計算錯誤率。
使用演算法:此步驟可以適用於任何監督學習任務,而使用決策樹可以更好地理解資料的內在含義。

決策樹優缺點

相對於其他資料探勘演算法,決策樹在以下幾個方面擁有優勢:

1 決策樹易於理解和實現.人們在通過解釋後都有能力去理解決策樹所表達的意義。
2 對於決策樹,資料的準備往往是簡單或者是不必要的.其他的技術往往要求先把資料一般化,比如去掉多餘的或者空白的屬性。
3 能夠同時處理資料型和常規型屬性。其他的技術往往要求資料屬性的單一。
4 是一個白盒模型如果給定一個觀察的模型,那麼根據所產生的決策樹很容易推出相應的邏輯表示式。
5 易於通過靜態測試來對模型進行評測。表示有可能測量該模型的可信度。
6 在相對短的時間內能夠對大型資料來源做出可行且效果良好的結果。
7 計算複雜度不高,輸出結果易於理解,資料有缺失也能跑,可以處理不相關特徵。

缺點:

1 容易過擬合。
2 對於那些各類別樣本數量不一致的資料,在決策樹當中資訊增益的結果偏向於那些具有更多數值的特徵。

適用資料型別:數值型和標稱型。

1 數值型:數值型目標變數則可以從無限的數值集合中取值,如0.100,42.001等 (數值型目標變數主要用於迴歸分析)
2 標稱型:標稱型目標變數的結果只在有限目標集中取值,如真與假(標稱型目標變數主要用於分類)

案例描述:加深決策樹理解

案例描述

小王是一家著名高爾夫俱樂部的經理。但是他被僱員數量問題搞得心情十分不好。某些天好像所有人都來玩高爾夫,以至於所有員工都忙的團團轉還是應付不過來,而有些天不知道什麼原因卻一個人也不來,俱樂部為僱員數量浪費了不少資金。小王的目的是通過下週天氣預報尋找什麼時候人們會打高爾夫,以適時調整僱員數量。因此首先他必須瞭解人們決定是否打球的原因。

資料採集

在2周時間內我們得到以下記錄:

天氣狀況有晴,雲和雨;氣溫用華氏溫度表示;相對溼度用百分比;還有有無風。當然還有顧客是不是在這些日子光顧俱樂部。最終他得到了14行5列的資料表格。


構建決策樹

決策樹模型就被建起來用於解決問題。


結果分析

決策樹是一個有向無環圖。根結點代表所有資料。分類樹演算法可以通過變數outlook,找出最好地解釋非獨立變數play(打高爾夫的人)的方法。變數outlook的範疇被劃分為以下三個組:晴天,多雲天和雨天。

我們得出第一個結論:如果天氣是多雲,人們總是選擇玩高爾夫,而只有少數很著迷的甚至在雨天也會玩。

接下來我們把晴天組的分為兩部分,我們發現顧客不喜歡溼度高於70%的天氣。最終我們還發現,如果雨天還有風的話,就不會有人打了。

這就通過分類樹給出了一個解決方案。小王(老闆)在晴天,潮溼的天氣或者颳風的雨天解僱了大部分員工,因為這種天氣不會有人打高爾夫。而其他的天氣會有很多人打高爾夫,因此可以僱用一些臨時員工來工作。

決策樹演算法實現與分析

案例: 判定魚類和非魚類

案例需求描述

我們採集海洋生物資料資訊,選擇其中5條如下表所示,從諸多特徵中選擇2個最主要特徵,以及判定是否屬於魚類(此處我們選擇二分類法即只考慮魚類和非魚類)。
根據這些資訊如何建立一個決策樹進行分類並可視化展示?

收集資料

部分資料採集資訊

序號不浮出水面是否可以生存是否有腳蹼屬於魚類
1
2
3
4
5

我們將自然語言資料轉化為計算機輸入資料,程式碼實現如下:

'''建立資料集,返回資料集和標籤'''
def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing', 'flippers']
    return dataSet, labels

執行檢視資料集的特徵向量和分類標籤:

# 1 列印資料集和標籤
dataset,label=createDataSet()
print(dataset)
print(label)

執行結果:

[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
['no surfacing', 'flippers']

準備資料

由於我們輸入的資料已經是資料預處理後的資料,這一步不需要進行。

分析資料

我們得到資料之後,到底是按照第一個特徵即(不浮出水面是否可以生存)還是第二個特徵即(是否有腳蹼)進行資料劃分呢?這裡面就需要找到一種量化的方法判斷特徵的選擇。在介紹具體資料劃分方法之前,我們首先明白劃分資料集的最大原則是:將無序的資料變得更加有序

1948 年,夏農引入資訊熵,將其定義為離散隨機事件的出現概率。一個系統越有序,資訊熵就越低;反之,一個系統越混亂,資訊熵就越高。所以說,資訊熵可以被認為是系統有序化程度的一個度量。

這裡就要用的資訊熵的概念,熵越高表示混合資料越多,度量資料集無序程度。我們看下資訊熵的數學描述(具體請自行查詢熵相關知識):

計算資料集的夏農熵(資訊期望值)

根據公式比較容易理解的實現方法1如下:

'''計算資料集的夏農熵(資訊期望值):熵越高表示混合資料越多,度量資料集無序程度'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet) # 計算資料集中例項總數
    labelCounts = {} # 建立字典,計算分類標籤label出現的次數
    for featVec in dataSet:
        currentLabel = featVec[-1] # 記錄當前例項的標籤
        if currentLabel not in labelCounts.keys():# 為所有可能的分類建立字典
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
        # print(featVec, labelCounts) # 列印特徵向量和字典的鍵值對

    # 對於label標籤的佔比,求出label標籤的夏農熵
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries # 計算類別出現的概率。
        shannonEnt -= prob * log(prob, 2) # 計算夏農熵,以 2 為底求對數
    print(Decimal(shannonEnt).quantize(Decimal('0.00000')))
    return shannonEnt

更高階的實現方法2如下:

'''計算資料集的夏農熵(資訊期望值):熵越高表示混合資料越多,度量資料集無序程度'''
def calcShannonEnt(dataSet):
    # 需要對 list 中的大量計數時,可以直接使用Counter,不用新建字典來計數
    label_count = Counter(data[-1] for data in dataSet) # 統計標籤出現的次數
    probs = [p[1] / len(dataSet) for p in label_count.items()] # 計算概率
    shannonEnt = sum([-p * log(p, 2) for p in probs]) # 計算夏農熵
    print(Decimal(shannonEnt).quantize(Decimal('0.00000')))
    return shannonEnt

呼叫執行如下:

# 2 計算資料集的熵
calcShannonEnt(dataset)

按照給定的特徵劃分資料集

我們根據資訊熵度量出來的特徵,進行資料集劃分方法1如下:

'''劃分資料集:按照特徵劃分'''
def splitDataSet(dataSet, index, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[index] == value:# 判斷index列的值是否為value
            reducedFeatVec = featVec[:index] # [:index]表示取前index個特徵
            reducedFeatVec.extend(featVec[index+1:]) # 取接下來的資料
            retDataSet.append(reducedFeatVec)
    print(retDataSet)
    return retDataSet

我們根據資訊熵度量出來的特徵,進行資料集劃分方法2如下:

'''劃分資料集:按照特徵劃分'''
def splitDataSet(dataSet, index, value):
    retDataSet = [data for data in dataSet for i, v in enumerate(data) if i == index and v == value]
    print(retDataSet)
    return retDataSet

指定特徵的資料集劃分方法呼叫

#3 劃分資料集
splitDataSet(dataset,0,1)

執行結果如下:

[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no']]

選擇最好的資料集劃分方式

選擇最好的資料集劃分方式:特徵選擇,劃分資料集、計算最好的劃分資料集特徵,方法1如下:

'''
注意:一是資料集列表元素具備相同資料長度,二是最後一列是標籤列
'''
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 # 特徵總個數, 最後一列是標籤
    baseEntropy = calcShannonEnt(dataSet) # 計算資料集的資訊熵
    bestInfoGain, bestFeature = 0.0, -1 # 最優的資訊增益值, 和最優的Featurn編號
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet] # 獲取各例項第i+1個特徵
        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
        print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    # print(bestFeature)
    return bestFeature

選擇最好的資料集劃分方式:特徵選擇,劃分資料集、計算最好的劃分資料集特徵,方法2如下:

'''
注意:一是資料集列表元素具備相同資料長度,二是最後一列是標籤列
'''
def chooseBestFeatureToSplit(dataSet):
    base_entropy = calcShannonEnt(dataSet) # 計算初始夏農熵
    best_info_gain = 0
    best_feature = -1
    # 遍歷每一個特徵
    for i in range(len(dataSet[0]) - 1):
        # 對當前特徵進行統計
        feature_count = Counter([data[i] for data in dataSet])
        # 計算分割後的夏農熵
        new_entropy = sum(feature[1] / float(len(dataSet)) * calcShannonEnt(splitDataSet(dataSet, i, feature[0])) for feature in feature_count.items())
        # 更新值
        info_gain = base_entropy - new_entropy
        # print('No. {0} feature info gain is {1:.3f}'.format(i, info_gain))
        if info_gain > best_info_gain:
            best_info_gain = info_gain
            best_feature = i
    # print(best_feature)
    return best_feature

選擇最好的資料集劃分方法呼叫

# 4 選擇最好的資料集劃分方式
chooseBestFeatureToSplit(dataset))

執行結果如下:

infoGain= 0.4199730940219749 bestFeature= 0 0.9709505944546686 0.5509775004326937
infoGain= 0.17095059445466854 bestFeature= 1 0.9709505944546686 0.8
選擇:0

訓練演算法:構造樹的資料結構

建立樹的函式程式碼如下:

'''建立決策樹'''
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # 如果資料集的最後一列的第一個值出現的次數=整個集合的數量,也就說只有一個類別,就只直接返回結果就行
    # 第一個停止條件:所有的類標籤完全相同,則直接返回該類標籤。
    # count() 函式是統計括號中的值在list中出現的次數
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果資料集只有1列,那麼最初出現label次數最多的一類,作為結果
    # 第二個停止條件:使用完了所有特徵,仍然不能將資料集劃分成僅包含唯一類別的分組。
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    # 選擇最優的列,得到最優列對應的label含義
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 獲取label的名稱
    bestFeatLabel = labels[bestFeat]
    # 初始化myTree
    myTree = {bestFeatLabel: {}}
    # 所以這行程式碼導致函式外的同名變數被刪除了元素,造成例句無法執行,提示'no surfacing' is not in list
    # del(labels[bestFeat])
    # 取出最優列,然後它的branch做分類
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 求出剩餘的標籤label
        subLabels = labels[:]
        # 遍歷當前選擇特徵包含的所有屬性值,在每個資料集劃分上遞迴呼叫函式createTree()
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
        # print('myTree', value, myTree)
    print(myTree)
    return myTree

其中多數表決方法決定葉子節點的分類實現如下:

'''多數表決方法決定葉子節點的分類:選擇出現次數最多的一個結果'''
def majorityCnt(classList):
    # -----------多數表決實現的方式一--------------
    # classCount = {}   # 標籤字典,用於統計類別頻率
    # for vote in classList: # classList標籤的列表集合
    #     if vote not in classCount.keys():
    #         classCount[vote] = 0
    #     classCount[vote] += 1
    # # 取出結果(yes/no),即出現次數最多的結果
    # sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    # print('sortedClassCount:', sortedClassCount)
    # return sortedClassCount[0][0]


    # -----------多數表決實現的方式二-----------------
    major_label = Counter(classList).most_common(1)[0]
    print('sortedClassCount:', major_label[0])
    return major_label[0]

呼叫方法:

# 6建立決策樹
createTree(dataset, label)

執行結果:

{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

結果分析:
此時,每次生成決策樹資料都需要大量的計算,並且耗時,最好是每次直接呼叫生成結果。這裡就需要使用Python模組pickle序列化物件,其儲存決策樹讀取決策樹程式碼實現如下:

'''使用pickle模組儲存決策樹'''
def storeTree(inputTree, filename):
    import pickle
    # -------------- 第一種方法 --------------
    fw = open(filename, 'wb')
    pickle.dump(inputTree, fw)
    fw.close()

    # -------------- 第二種方法 --------------
    with open(filename, 'wb') as fw:
        pickle.dump(inputTree, fw)


def grabTree(filename):
    import pickle
    fr = open(filename,'rb')
    return pickle.load(fr)

測試演算法:使用決策樹執行分類

用決策樹進行魚類屬於分類實現如下:

'''用決策樹分類函式'''
def classify(inputTree, featLabels, testVec):
    firstStr = list(inputTree.keys())[0] # 獲取tree的根節點對於的key值
    secondDict = inputTree[firstStr]  # 通過key得到根節點對應的value
    # 判斷根節點名稱獲取根節點在label中的先後順序,這樣就知道輸入的testVec怎麼開始對照樹來做分類
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]).__name__ == 'dict':
                classLabel = classify(secondDict[key], featLabels, testVec)
            else:
                classLabel = secondDict[key]
    print(classLabel)
    return classLabel

呼叫方法:

# 7 用決策樹分類函式
myTree = treePlotter.retrieveTree(0)
# print(myTree)
classify(myTree,label,[1,0])

執行結果:

分類結果:no surfacing

決策樹分類器實現

使用演算法此步驟可以適用於任何監督學習任務,而使用決策樹可以更好地理解資料的內在含義。

'''決策樹判斷是否是魚'''
def fishTest():
    # 1.建立資料和結果標籤
    myDat, labels = createDataSet()

    # 計算label分類標籤的夏農熵
    calcShannonEnt(myDat)

    # 求第0列 為 1/0的列的資料集【排除第0列】
    print('1---', splitDataSet(myDat, 0, 1))
    print('0---', splitDataSet(myDat, 0, 0))

    # 計算最好的資訊增益的列
    print(chooseBestFeatureToSplit(myDat))

    import copy
    myTree = createTree(myDat, copy.deepcopy(labels))
    print(myTree)
    # [1, 1]表示要取的分支上的節點位置,對應的結果值
    print(classify(myTree, labels, [1, 1]))

    # 畫圖視覺化展現
    treePlotter.createPlot(myTree)

呼叫決策樹分類方法:

# 9 決策樹判斷是否是魚
fishTest()

執行結果如下:

1--- [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no']]
0--- [[0, 1, 'no'], [0, 1, 'no']]
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
yes

視覺化結果


決策樹實際應用:預測隱形眼鏡的測試程式碼

專案概述

隱形眼鏡型別包括硬材質、軟材質以及不適合佩戴隱形眼鏡。我們需要使用決策樹預測患者需要佩戴的隱形眼鏡型別。

開發流程

收集資料: 提供的文字檔案。
解析資料: 解析 tab 鍵分隔的資料行
分析資料: 快速檢查資料,確保正確地解析資料內容,使用 createPlot() 函式繪製最終的樹形圖。
訓練演算法: 使用 createTree() 函式。
測試演算法: 編寫測試函式驗證決策樹可以正確分類給定的資料例項。
使用演算法: 儲存樹的資料結構,以便下次使用時無需重新構造樹。
收集資料:提供的文字檔案

資料讀取

文字檔案資料格式如下:

young    myope    no    reduced    no lenses
young    myope    no    normal    soft
young    myope    yes    reduced    no lenses
young    myope    yes    normal    hard
young    hyper    no    reduced    no lenses
young    hyper    no    normal    soft
young    hyper    yes    reduced    no lenses
young    hyper    yes    normal    hard
pre    myope    no    reduced    no lenses
pre    myope    no    normal    soft
pre    myope    yes    reduced    no lenses
pre    myope    yes    normal    hard
pre    hyper    no    reduced    no lenses
pre    hyper    no    normal    soft
pre    hyper    yes    reduced    no lenses
pre    hyper    yes    normal    no lenses
presbyopic    myope    no    reduced    no lenses
presbyopic    myope    no    normal    no lenses
presbyopic    myope    yes    reduced    no lenses
presbyopic    myope    yes    normal    hard
presbyopic    hyper    no    reduced    no lenses
presbyopic    hyper    no    normal    soft
presbyopic    hyper    yes    reduced    no lenses
presbyopic    hyper    yes    normal    no lenses

程式碼實現: 編寫測試函式驗證決策樹可以正確分類給定的資料例項。

'''預測隱形眼鏡的測試程式碼'''
def ContactLensesTest():
    # 載入隱形眼鏡相關的 文字檔案 資料
    fr = open('lenses.txt')
    # 解析資料,獲得 features 資料
    lenses = [inst.strip().split('    ') for inst in fr.readlines()]
    # 得到資料的對應的 Labels
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    # 使用上面的建立決策樹的程式碼,構造預測隱形眼鏡的決策樹
    lensesTree = createTree(lenses, lensesLabels)
    print(lensesTree)
    # 畫圖視覺化展現
    treePlotter.createPlot(lensesTree)

執行結果

呼叫方法

# 10 預測隱形眼鏡型別
ContactLensesTest()

執行結果

{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'no': {'age': {'young': 'soft', 'pre': 'soft', 'presbyopic': {'prescript': {'myope': 'no lenses', 'hyper': 'soft'}}}}, 'yes': {'prescript': {'myope': 'hard', 'hyper': {'age': {'young': 'hard', 'pre': 'no lenses', 'presbyopic': 'no lenses'}}}}}}}}

決策樹視覺化


完整程式碼下載

機器學習和自然語言QQ群:436303759。微信公眾號:datathinks


自然語言處理和機器學習QQ交流群:436303759 自然語言處理和機器學習微信公眾號:datathinks

原始碼請進QQ群檔案下載:


書籍推薦



自然語言處理和機器學習:唐聃 白寧超 馮暄 等著

自然語言處理理論與實戰】由清華大學教授、博士生導師;電子科技大學教授、博士生導師;中國科學院研究員、博士生導師;百度企業智慧平臺,大資料高階工程師;美團點評,大資料研發工程師;英國哈德斯菲爾德大學,人工智慧博士聯合推薦。可在京東淘寶亞馬遜噹噹等網站購買....

作者宣告