Machine Learning In Action -- ID3決策樹學習算法的python實現

decision tree Learning 決策樹學習筆記

決策樹學習是一種相對比較簡單的分類學習方法,但是分類效果較好並且表示直觀,主要針對離散型目標,它也等價於用if-then規則表示。


決策樹學習在Mitchell的《機器學習》第三章中有詳細講解,也有許多人做了詳細的筆記,比如http://www.cnblogs.com/pangxiaodong/archive/2011/05/11/2042882.html 。這裏主要簡單介紹最經典的決策樹學習算法ID3的python實現。


1. 熵(Entropy)

      決策樹中用到了信息論中熵的概念,熵主要表示的是體系的混亂程度,也稱不確定程度。在信息論中,熵可以理解爲對於信息S,用統一的概率表示,至少需要多少位來編碼才能清楚的表示該信息。(這裏概率的意思可以理解爲:設S爲全樣本空間,可以根據某一組值(也就是我們的分類值和預測值)來標記S爲S1...Sn,每一份在全樣本空間中所佔的比率。) 熵的公式爲:


    舉個例子:設S爲全體實數空間,以正、負屬性來標記S,則屬性正、負的概率各爲1/2,根據公式可以得到Entropy(S)=1,即我們需要1位來表示屬性是正還是負;而如果S爲正實數時,屬性正、負的概率分別爲1和0,Entropy(S)=0,即我們不需要任何位來表示該屬性,因爲它一定是正的。這就是熵的含義,取值範圍爲[0,1],值越大表示樣本空間不確定性越高。


2.  信息增益(Information Gain)

      理解清楚熵的含義後,我們可以很容易理解信息增益的概念。S依然爲樣本空間,設A是S中的某一屬性,當我們用屬性A劃分S時,會根據在A上的不同值將S劃分爲不同的子樣本空間S1,S2…,然後我們計算所有子樣本空間的熵Entropy(Si),以及Si佔S的比例,並用類似全概率公式的方法求劃分後的結果的熵Entropy(S’)。從信息論的角度看,這個熵就是用Entropy(S’)位編碼才能表示劃分後的結果。

     由此,我們可以得到信息增益的值Gain(S,A),就是用劃分前的熵Entropy(S)減去劃分後的熵Entropy(S’)。即公式


      那麼這個值是越大越好,還是越小越好呢?根據上述討論,當然是劃分後的熵越小越好,越小表示劃分後的結果越有序,也就越有利於我們進行決策;那麼反過來,Gain(S,A)的結果越大說明屬性A的劃分效果越好,我們就越應該優先用A來劃分樣本空間S。

     

      OK,這就是最經典的決策樹算法ID3的核心問題,即每次劃分時哪個屬性的劃分效果最好?答案就是依次用沒有用過的屬性去劃分父空間(因爲涉及到遞歸),然後用信息增益最大的屬性作爲當前劃分屬性。


3.   求解決策樹的思想和流程

     1)首先,我們知道決策樹的建立是一個遞歸過程,那麼遞歸的結束條件是什麼呢?a)如果所有的屬性都已用來劃分,那麼說明決策樹建立已經完成,則結束;b)如果待劃分空間內所有樣本都屬於同一分類,那麼說明在該空間內已不需要再繼續劃分,則結束。

     2)然後,如果遞歸沒有結束,則計算各屬性劃分的信息增益,並選擇信息增益值最大的屬性作爲當前劃分屬性,並寫入存儲劃分屬性的數據結構中。

     3)最後,我們要對當前劃分屬性的每個值,再繼續執行遞歸建立決策樹的過程。


     這就是ID3決策樹學習算法的主要流程,下面將《Machine Learning In Action》中的python實現代碼學習着實現了一遍,如下所示:

<span style="font-family:SimSun;font-size:12px;">#coding=utf-8
'''
Created on Sep 13, 2014

Aim: To test decision tree algorithm 

@author: lemon
'''

# 要用到log運算
from math import log
import operator
import types

'''
   函數功能:計算給定集合的熵
   輸入:給定數據集
   輸出:熵的值
'''
def calculateEntropy(dataSet):
    # 計算數據集共有多少元組
    numOfObj = len(dataSet)
    
    # 用labelCounts存儲數據集的分類標籤
    labelCounts = {}
    
    # 遍歷數據集
    for obj in dataSet:
        # 取分類標籤
        currentLabel = obj[-1]
        # 如果標籤不在labelCounts中,則在labelCounts中插入
        # 該標籤鍵,並設值爲0
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        # 該標籤值 +1
        labelCounts[currentLabel] += 1
    
    entropy = 0.0
    # 遍歷labelCounts,根據熵的公式計算值
    for label in labelCounts:
        # 該標籤在總數據集中佔的概率
        prob = float(labelCounts[label])/numOfObj
        entropy -= prob * log(prob,2)
    return entropy

'''
    函數功能:根據劃分屬性軸和某一屬性值,得到劃分後的數據子集
    輸入:給定數據集dataSet,劃分屬性軸axis,給定屬性值value
    輸出:劃分後的數據子集
'''
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    # 遍歷dataSet中的所有對象,如果其在axis軸上值爲value
    # 則將該元組存入retDataSet[],除了該軸數據
    for obj in dataSet:
        if obj[axis] == value:
            reducedObj = obj[:axis]
            reducedObj.extend(obj[axis+1:])
            retDataSet.append(reducedObj)
    return retDataSet

'''
    函數功能:選擇劃分效果最好的屬性
    輸入:給定數據集
    輸出:劃分屬性軸的下標
'''
def bestFeatureToSplit(dataSet):
    # 初始化信息增益和劃分屬性的軸下標
    maxInfoGain = 0.0
    splitAxis = -1
    
    # 計算給定數據集的熵
    wholeEntropy = calculateEntropy(dataSet)
    
    # 計算共有多少屬性,-1的含義是去掉分類標籤那一列
    featureNum = len(dataSet[0]) - 1
    
    # 計算共有多少元組
    trainingNum = len(dataSet)
    
    # 對每一個屬性,對其進行劃分,並計算信息增益,如果該信息增益比
    # 當前記錄的信息增益值大,則更新;否則,繼續下一個屬性
    for i in range(featureNum):
        # 獲得屬性軸爲i的所有屬性值,並存儲在featureList中
        '''
            注意這裏書上用兩行代碼實現,更簡單
            featureList = [example[j] for example in dataSet]
            uniqueVals = set(featureList)   # set is unique
        '''
        featureList = []
        for j in range(trainingNum):
            if dataSet[j][i] not in featureList:
                featureList.append(dataSet[j][i])
            else:
                continue
            
        # 現在已獲得了第i+1列屬性的所有值,計算共有多少不同值
        iFeatureNum = len(featureList)
        
        # 計算劃分後所有數據子集的熵和
        splitedEntropy = 0.0
        for k in range(iFeatureNum):
            # 獲得劃分子集
            retDataSet = splitDataSet(dataSet, i, featureList[k])
            # 得到子集元組數量
            retDataSetNum = len(retDataSet)
            # 得到子集熵
            retDataSetEntropy = calculateEntropy(retDataSet)
            # 累加
            splitedEntropy += (float(retDataSetNum)/float(trainingNum))*retDataSetEntropy
        
        # 判斷當前劃分的信息增益是否大於maxInfoGain
        if (wholeEntropy - splitedEntropy) > maxInfoGain:
            maxInfoGain = wholeEntropy - splitedEntropy
            splitAxis = i
        else:
            continue
    
    return splitAxis
            
'''
    函數功能:當劃分結束時,如果某一子集中的所有分類標籤還不相同,那麼
            用這個函數,來選擇該空間中某一標籤值最多的作爲標籤,與kNN
            算法中的相同
    輸入:標籤list
    輸出:標籤值最多的標籤
'''
def majorityCount(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    # sort
    sortedClassCount = sorted(classCount.iteritems(),
                              key=operator.itemgetter(1),reverse=True)
    
    return sortedClassCount[0][0]

'''
    函數功能:遞歸建立決策樹
    輸入:給定數據集dataSet,劃分屬性名稱labels
    輸出:決策樹(dictionary格式)
'''
def createTree(dataSet,labels):
    # 獲得該dataSet的所有標籤
    classList = [example[-1] for example in dataSet]
    
    # 結束條件1:所有標籤相同,則返回該標籤
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 結束條件2:所有屬性已經劃分,返回最多數量的標籤
    if len(dataSet[0]) == 1:
        return majorityCount(classList)
    
    # 若沒結束,則劃分
    bestFeature = bestFeatureToSplit(dataSet)
    bestFeatureLabel = labels[bestFeature]
    
    # 用dictionary格式存儲該劃分
    myTree = {bestFeatureLabel:{}}  
    # 從labels中刪除該屬性
    del(labels[bestFeature])
            
    # 獲得該劃分屬性的所有屬性值
    featureValues = [example[bestFeature] for example in dataSet]
    uniqueVals = set(featureValues)  # 取唯一值
    
    # 對每一屬性值遞歸求決策樹
    for value in uniqueVals:
        # 用subLabels存儲labels的值是因爲,遞歸的時候會修改list的內容
        subLabels = labels[:]
        myTree[bestFeatureLabel][value] = createTree(splitDataSet\
                                                     (dataSet,bestFeature,value),subLabels)
    
    return myTree

'''
    函數功能:創建數據集和屬性名稱
    輸出:返回數據集和屬性名稱list
'''
def createDataset():
    dataSet = [['Sunny','Hot','High','Weak','No'],
               ['Sunny','Hot','High','Strong','No'],
               ['Overcast','Hot','High','Weak','Yes'],
               ['Rain','Mild','High','Weak','Yes'],
               ['Rain','Cool','Normal','Weak','Yes'],
               ['Rain','Cool','Normal','Strong','No'],
               ['Overcast','Cool','Normal','Strong','Yes'],
               ['Sunny','Mild','High','Weak','No'],
               ['Sunny','Cool','Normal','Weak','Yes'],
               ['Rain','Mild','Normal','Weak','Yes'],
               ['Sunny','Mild','Normal','Strong','Yes'],
               ['Overcast','Mild','High','Strong','Yes'],
               ['Overcast','Hot','Normal','Weak','Yes'],
               ['Rain','Mild','High','Strong','No'],]
    labels = ['Outlook','Temperature','Humidity','Wind']
    return dataSet, labels

dataSet, labels = createDataset()

labelsCopy = labels[:]

decisionTree = createTree(dataSet,labels)
print (decisionTree)

'''
    函數功能:根據得到的決策樹進行預測
    輸入:決策樹inputTree,屬性名稱列表featLabels,測試數據testVector
    輸出:預測分類標籤
'''
def classify(inputTree, featLabels, testVector):
    # 決策樹是dictionary格式,所以取第一個鍵firstStr和值secondDict
    firstStr = tuple(inputTree.keys())[0]
    secondDict = inputTree[firstStr]
    
    # 根據鍵(屬性)獲得其軸下標
    featIndex = featLabels.index(firstStr)
    
    # 循環遞歸判斷
    for key in secondDict.keys():
        if testVector[featIndex] == key:
            # 如果鍵的類型是dict,說明是dictionary,還需遞歸判斷
            if type(secondDict[key]).__name__ == 'dict':
                classLabel = classify(secondDict[key],featLabels,testVector)
            # 否則,判斷結束,直接返回
            else:
                classLabel = secondDict[key]
    
    return classLabel

classLabel = classify(decisionTree,labelsCopy,['Sunny','Mild','High','Weak'])
print(classLabel)


</span>

程序的結構很簡單,創建決策樹的主函數是createTree(),分類函數是classify(),註釋也比較詳細,較好理解,值得提的是,createTree()函數返回的結果是嵌套的dictionary格式,比如{'outLook':{‘a’:{''...}, 'b': 'yes'}...}。然後classify()就是按照這個結構進行分類。這裏對其中遇到的新用法記錄一下:

1)其中兩次用到了featureValues = [example[bestFeature] for example in dataSet]這種用法,相對於我用的for然後判定要簡潔的多,以後多用這種用法來填充list;

2)其中用到了type()這個用法,在classify()函數中,它主要是返回括號內變量的類型,然後用classes.__name__用法來獲取變量的類型名稱,從而進行比較;


其實在《機器學習》書中,還有一些關於ID3算法的優化和討論,以及C4.5算法等,在上面提到的那個筆記中有簡單介紹,後面有時間的話會整理一篇關於決策樹的算法討論出來。



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