【基礎算法】決策樹演算法(Decision Tree)概述及Python實踐

決策樹演算法

原理:透過特徵值構成樹結構來分類,找出在劃分數據集時找出哪個特徵起到了決定性的作用。

目標:分類未知類別的案例,用以理解數據中所蘊涵的知識訊息

優點:計算複雜度不高,輸出結果易於瞭解,對中間值的缺失不敏感、可以處理不相關的特徵數據

缺點:可能有過度匹配的問題

適用數據:數值型和標稱型

輸入:未知類別的實例

輸出:輸入實例的類別


工作原理

從數據集中提取出一系列規則,在每次劃分前找出在劃分數據集時找出哪個特徵起到了決定性的作用,依照找特徵的規則不同而有不同的決策樹算法(ID3、C4.5、CART),進而評估每個特徵。若某個分支下的數據屬於同一個類型則無需進一步對數據集分割;若不屬於同一個類型則重複劃分子集,直到所有具有相同類型的數據均在一個數據子集內,每個葉節點代表一個類別。

創建分支的僞代碼函數createBranch():

檢查數據集中的每個實例是否屬於同一個分類:
If so return類標籤
Else
		尋找劃分數據集的最好特徵
		劃分數據集
		創建分支節點
				for 每個劃分的子集
						調用函數createBranch並增加返回結果到分支節點中
		return 分支節點

劃分數據集的大原則是:將無序的數據變得更加有序,在劃分數據集前後信息發生的變化稱謂"信息增益(Information Gain)",透過信息增益選信息增益最高的特徵就是最好的劃分選擇。

熵(Entropy)爲信息的期望值,爲了計算熵,我們要計算所有類別中所有可能值包含的信息期望值。


ID3算法

ID3算法(Iterative Dichotomiser 3,迭代二叉樹3代)是一種貪心算法,用來構造決策樹。

對於有K個類別的分類問題來說,假定樣本集合D中第 k 類樣本所佔的比例爲pk(k=1,2,…,K),則樣本集合D的信息熵定義爲:
Ent(D)=- \sum_{k=1}^{K} p_k\cdot  log_{2}p_k
其中pk是選擇該分類的概率。

信息增益(information gain)是針對一個特定的分支標準(branching criteria)T,計算原有數據的信息熵與引入該分支標準後的信息熵之差。

在這裏插入圖片描述
其中a是有V個不同取值的離散特徵,使用特徵a對樣本集D進行劃分會產生V個分支,Dv表示D中所有在特徵a上取值爲av的樣本,即第v個分支的節點集合。|Dv||D||Dv||D|表示分支節點的權重,即分支節點的樣本數越多,其影響越大。

ID3算法的基本思想是:首先計算出原始數據集的信息熵,然後依次將數據中的每一個特徵作爲分支標準,並計算其相對於原始數據的信息增益,選擇最大信息增益的分支標準來劃分數據,因爲信息增益越大,區分樣本的能力就越強,越具有代表性。重複上述過程從而生成一棵決策樹,很顯然這是一種自頂向下的貪心策略。


C4.5算法

對ID3算法的改進,C4.5克服了ID3的2個缺點:

  1. 用信息增益選擇屬性時偏向於選擇分枝比較多的屬性值,即取值多的屬性
  2. 不能處理連續屬性

C4.5算法不不直接使用信息增益,而是使用“增益率”(gain ratio)來選擇最優的分支標準,增益率的定義如下:
在這裏插入圖片描述
在這裏插入圖片描述稱作分支標準T的“固有值”(intrinstic value)。作爲分支標準的屬性可取值越多,則IV 的值越大。

需要注意的是: 增益率準則對可取值數目較少的屬性有所偏好,
因此C4.5算法並不是直接選擇增益率最大的屬性作爲分支標準,
而是先從候選屬性中找出信息增益高於平均水平的屬性,
再從中選擇增益率最高的。

CART算法

CART(Classification And Regression Tree)可用於創建分類樹和迴歸樹。CART算法的特點:

  1. 二分(Binary Split):在每次判斷過程中,都是對樣本數據進行二分。CART算法是一種二分遞歸分割技術,把當前樣本劃分爲兩個子樣本,使得生成的每個非葉子結點都有兩個分支,因此CART算法生成的決策樹是結構簡潔的二叉樹。由於CART算法構成的是一個二叉樹,它在每一步的決策時只能是“是”或者“否”,即使一個feature有多個取值,也是把數據分爲兩部分
  2. 單變量分割(Split Based on One Variable):每次最優劃分都是針對單個變量。
  3. 剪枝策略:CART算法的關鍵點,也是整個Tree-Based算法的關鍵步驟。剪枝過程特別重要,所以在最優決策樹生成過程中佔有重要地位。有研究表明,剪枝過程的重要性要比樹生成過程更爲重要,對於不同的劃分標準生成的最大樹(Maximum Tree),在剪枝之後都能夠保留最重要的屬性劃分,差別不大。反而是剪枝方法對於最優樹的生成更爲關鍵。

CART的分支標準建立在GINI指數這個概念上,GINI指數主要是度量數據劃分的不純度,是介於0~1之間的數。GINI值越小,表明樣本集合的純淨度越高;GINI值越大表明樣本集合的類別越雜亂。直觀來說,GINI指數反映了從數據集中隨機抽出兩個樣本,其類別不一致的概率


實踐

首先創建一個打好標的訓練集

# 創建數據集
def createDataSet():
    dataSet=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
    labels=['A','B']
    return dataSet,labels

此時可以在Python Shell中將檔案導入後使用
在這邊筆者的檔案叫做"DecisionTree.py"
故在Python Shell 中將位置移動到檔案的位置後輸入

>>> import DecisionTree
>>> dataSet,labels=DecisionTree.createDataSet()

即將DecisionTree的函數createDataSet()返回的值分別放入dataSet和labels
之後可以在Python Shell中查看有沒有成功導入

>>> dataSet
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['A', 'B']

下面是示範圖
在這裏插入圖片描述
計算信息熵(Entropy)

# 計算熵
def calcShannonEnt(dataSet):
    # numEntries:標籤個數
    numEntries = len(dataSet)
    #爲所有的分類類目創建字典
    labelCounts ={}
    for featVec in dataSet:
        #取得最後一列數據(標籤)
        currentLable=featVec[-1]
        if currentLable not in labelCounts.keys():
            labelCounts[currentLable]=0
        labelCounts[currentLable]+=1
    #計算熵
    shannonEnt=0.0
    for key in labelCounts:
        # 計算每一個標籤的概率
        prob = float(labelCounts[key]) / numEntries
        # 計算熵
        shannonEnt -= prob * np.math.log(prob, 2)
    return shannonEnt

在Python Shell中輸入函數calcShannonEnt()並導入數據集即可得到熵

>>> DecisionTree.calcShannonEnt(dataSet)
0.9709505944546686

再來依照分類規則的不同分成ID3、C4.5、CART
先示範ID3的做法
底下爲公式的實踐

分類函數

# 定義按照某個特徵進行劃分的函數splitDataSet(求Gain時使用)
# 輸入三個變量(待劃分的數據集,特徵,分類值)
# 選出在該特徵有該分類的數據,再返回不含劃分特徵的子集

def splitDataSet(dataSet,axis,value):
    # retDataSet:分割後的數據集
    retDataSet=[]
    #featVec:特徵向量
    for featVec in dataSet:
        if featVec[axis]==value : 
            # 重組的reduceFeatVec
            reduceFeatVec=featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            # 將reduceFeatVec加入retDataSet
            retDataSet.append(reduceFeatVec)
    # 選出在該特徵有該分類的數據,再返回不含劃分特徵的子集
    return retDataSet

測試
在這裏插入圖片描述

>>> DecisionTree.splitDataSet(dataSet,0,0)
[[1, 'no'], [1, 'no']]
>>>  DecisionTree.splitDataSet(dataSet,0,1)
 [[1, 'yes'], [1, 'yes'], [0, 'no']]
 >>> DecisionTree.splitDataSet(dataSet,1,0)
 [[1, 'no']]
 >>> DecisionTree.splitDataSet(dataSet,1,1)
 [[1, 'yes'], [1, 'yes'], [0, 'no'], [0, 'no']]

再來是計算信息增益的部分
並用信息增益求得最佳劃分特徵

# 定義按照最大信息增益劃分數據的函數
def chooseBestFeatureToSplit(dataSet):
    # numFeature:特徵數量個數
    numFeature=len(dataSet[0])-1
    # 計算香農熵
    baseEntropy=calcShannonEnt(dataSet)
    # 重設信息增益
    bestInforGain=0
    bestFeature=-1
    for i in range(numFeature):
        # 得到某個特徵下所有值(某列)
        featList=[number[i] for number in dataSet]
        # uniqualVals:無重複的屬性特徵值
        uniqualVals=set(featList)
        newEntropy=0
        for value in uniqualVals:
            subDataSet=splitDataSet(dataSet,i,value)
            prob=len(subDataSet)/float(len(dataSet))
            # 對各子集香農熵求和
            newEntropy+=prob*calcShannonEnt(subDataSet)
        # 計算信息增益
        infoGain=baseEntropy-newEntropy
        #最大信息增益
        if (infoGain>bestInforGain):
            bestInforGain=infoGain
            bestFeature=i
    # 返回最佳劃分特徵值        
    return bestFeature

測試一下

>>> DecisionTree.chooseBestFeatureToSplit(dataSet)
0

再來是透過每次劃分後構建決策樹
底下開始決策樹構造函數

import operator
def majorityCnt(classList):
    classCount={}
    # 投票表決代碼
    for vote in classList:
        if vote not in classCount.keys():classCount[vote]=0
        classCount[vote]+=1
    sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)
    return sortedClassCount[0][0]

# 搭建決策樹
# 輸入:數據集和標籤
def createTree(dataSet,labels):
    classList=[example[-1] for example in dataSet]
    #類別相同,停止劃分
    if classList.count(classList[-1])==len(classList):
        return classList[-1]
    #長度爲1,返回出現次數最多的類別
    if len(classList[0])==1:
        return majorityCnt(classList)
    #按照信息增益最高選取分類特徵屬性
    #返回分類的特徵序號
    bestFeat=chooseBestFeatureToSplit(dataSet)
    #該特徵的label
    bestFeatLable=labels[bestFeat]
    #構建樹的字典
    myTree={bestFeatLable:{}}
    #從labels的list中刪除該label
    del(labels[bestFeat])
    featValues=[example[bestFeat] for example in dataSet]
    uniqueVals=set(featValues)
    for value in uniqueVals:
        # 子集合
        subLables=labels[:]
        # 構建數據的子集合,並進行遞歸
        myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables)
    return myTree

在重載DeciosionTree.py後,在Python Shell中測試一下分類效果

>>> DT=DecisionTree.createTree(dataSet,labels)
>>> DT
{'A': {0: 'no', 1: {'B': {0: 'no', 1: 'yes'}}}}

返回的是一個多層嵌套的字典。

將決策樹用於分類

# 輸入三個變量(決策樹,屬性特徵標籤,測試的數據)
# 決策樹:由createTree()函數創建
# 屬性特徵標籤:標籤
# 測試數據:隨機放一個數據看在這棵樹中的分類結果

def classify(inputTree,featLables,testVec):
    # 獲取樹的第一個特徵屬性
    firstStr=list(inputTree.keys())[0]
    # 樹的分支,子集合Dict
    secondDict=inputTree[firstStr]
    # 獲取決策樹第一層在featLables中的位置
    featIndex=featLables.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex]==key:
            if type(secondDict[key]).__name__=='dict':
                classLabel=classify(secondDict[key],featLables,testVec)
            else:classLabel=secondDict[key]
    return classLabel

在Python Shell中測試一下分類的結果
可以配合一下數據集狀況學習
在這裏插入圖片描述

# 當數據是[1,0]時,得到的分類結果是'no'
>>> DecisionTree.classify(DT,labels,[1,0])
'no'
>>> DecisionTree.classify(DT,labels,[0,0])
'no'
>>> DecisionTree.classify(DT,labels,[1,1])
'yes'
>>> DecisionTree.classify(DT,labels,[0,1])
'no'

儲存決策樹模型

若每次要使用決策樹進行分類時相當費時費力,故可以將已經搭建好的決策樹(像是字典形式)存儲起來保存在硬盤,在需要使用時拉出來使用。

# 存儲函數
def storeTree(inputTree,filename):
    import pickle
    #pickle默認方式是二進制,需要制定'wb'
    fw=open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()
# 讀取函數
def grabTree(filename):
    import pickle
    fr=open(filename,'rb')#需要制定'rb',以byte形式讀取
    return pickle.load(fr)

一般來說將決策樹存儲成’.txt’的文檔,等到需要時直接調出來使用。
再來在Python Shell中測試一下
重新加載’DecisionTree.py’後輸入

>>> DT
{'A': {0: 'no', 1: {'B': {0: 'no', 1: 'yes'}}}}
>>> DecisionTree.storeTree(DT,'classifierStorage.txt')

便會在Python Shell目錄底下生成一個.txt文檔‘classifierStorage.txt’

導入的部分也測試一下

>>> DecisionTree.grabTree('classifierStorage.txt')
{'A': {0: 'no', 1: {'B': {0: 'no', 1: 'yes'}}}}

這樣就完成了以ID3爲決策的決策樹
再來我們將決策樹可視化

繪製決策樹

這邊需使用matplotlib包,詳細安裝流程再看其他網上教學

再來是繪製決策樹的部分

# 導入matplotlib包
import matplotlib.pyplot as plt
from pylab import *  
# 否則中文無法正常顯示
mpl.rcParams['font.sans-serif'] = ['SimHei']

# 決策樹樣式部分
# 決策點樣式
decisionNode=dict(boxstyle='sawtooth',fc='0.8')
# 葉節點樣式
leafNode=dict(boxstyle='round4',fc='0.8')
# 箭頭樣式
arrow_args=dict(arrowstyle='<-')

# 畫節點
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 createPlot():
    # 使用plt.figure定義一個圖像窗口,編號爲1
    fig=plt.figure(1)
    createPlot.ax1=plt.subplot(111,frameon=False)
    plotNode('決策節點',(0.5,0.1),(0.1,0.5),decisionNode)
    plotNode('葉節點',(0.8,0.1),(0.3,0.8),leafNode)
    plt.show()

#測試
#獲取葉節點數量(廣度)
def getNumLeafs(myTree):
    numLeafs=0
    #'dict_keys' object does not support indexing
    firstStr=list(myTree.keys())[0]
    secondDict=myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':
            numLeafs+=getNumLeafs(secondDict[key])
        else:numLeafs+=1
    return numLeafs

#獲取樹的深度的函數(深度)
def getTreeDepth(myTree):
    maxDepth=0
    firstStr=list(myTree.keys())[0]
    secondDict=myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':
            thisDepth=1+getTreeDepth(secondDict[key])
        else: thisDepth=1
        if thisDepth > maxDepth:
            maxDepth=thisDepth
    return maxDepth

#定義一個預先創建樹的函數
def retrieveTree(i):
    listOfTrees=[{'A': {0: 'no', 1: {'B': {0: 'no', 1: 'yes'}}}},
                 {'A': {0: 'no', 1: {'B': {0: {'head':{0:'no', 1: 'yes'}},1:'no'}}}}
                 ]
    return listOfTrees[i]

#定義在父子節點之間填充文本信息的函數
def plotMidText(cntrPt,parentPt,txtString):
    xMid=(parentPt[0]-cntrPt[0])/2+cntrPt[0]
    yMid=(parentPt[1]-cntrPt[1])/2+cntrPt[1]
    createPlot.ax1.text(xMid,yMid,txtString)

#定義樹繪製的函數    
def plotTree(myTree,parentPt,nodeTxt):
    numLeafs=getNumLeafs(myTree)
    depth=getTreeDepth(myTree)
    firstStr=list(myTree.keys())[0]
    cntrPt=(plotTree.xOff+(1.0+float(numLeafs))/2/plotTree.totalW,plotTree.yOff)
    plotMidText(cntrPt,parentPt,nodeTxt)
    plotNode(firstStr,cntrPt,parentPt,decisionNode)
    secondDict=myTree[firstStr]
    plotTree.yOff=plotTree.yOff -1/plotTree.totalD
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':
            plotTree(secondDict[key],cntrPt,str(key))
        else:
            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/plotTree.totalD

 #定義主函數,來調用其它函數   
def createPlot(inTree):
    fig=plt.figure(1,facecolor='white')
    fig.clf()
    axprops=dict(xticks=[],yticks=[])
    createPlot.ax1=plt.subplot(111,frameon=False,**axprops)
    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()

這邊我們可以使用函數中制定的retrieveTree()來測試或者使用我們上面用的決策樹對象DT來測試
在Python Shell中輸入

>>> TT=retrieveTree(0)
>>> createPlot(TT)
>>> createPlot(DT)

則會跳出由matplotlib繪製的彈窗顯示這棵樹每個節點的分類規則
在這裏插入圖片描述

使用決策樹預測隱形眼鏡類型

隱形眼鏡的數據集包含了患者的四個屬性age,prescript,stigmatic,tearRate,利用這些數據構建決策樹,並通過Matplotlib繪製出決策樹的樹狀圖。

在Python Shell中輸入

# 導入檔案
>>>fr=open('lenses.txt')
# 讀取數據
>>> lenses=[inst.strip().split('\t') for inst in fr.readlines()]

此時數據集是這樣
在這裏插入圖片描述
導入標籤

# 導入標籤
>>> lensesLabels=['age','prescript','astigmatic','tearRate']

創建決策樹

# 創建決策樹
>>> lensesTree=DecisionTree.createTree(lenses,lensesLabels)
# 打印決策樹
>>> lensesTree
{'tearRate': {'reduced': 'no lenses',
  'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'presbyopic': 'no lenses',
        'young': 'hard',
        'pre': 'no lenses'}},
      'myope': 'hard'}},
    'no': {'age': {'presbyopic': {'prescript': {'hyper': 'soft',
        'myope': 'no lenses'}},
      'young': 'soft',
      'pre': 'soft'}}}}}}
 
 #展示決策樹
 >>>  DecisionTree.createPlot(lensesTree)

決策樹分類結果如圖
在這裏插入圖片描述

這樣就完成了ID3決策樹!

數據集及詳細代碼放在個人GitHub中歡迎參閱、星標,若有疑問歡迎留言討論。

個人GitHub

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章