1. 程式人生 > >機器學習實戰(Machine Learning in Action)學習筆記————03.決策樹原理、原始碼解析及測試

機器學習實戰(Machine Learning in Action)學習筆記————03.決策樹原理、原始碼解析及測試

機器學習實戰(Machine Learning in Action)學習筆記————03.決策樹原理、原始碼解析及測試

關鍵字:決策樹、python、原始碼解析、測試
作者:米倉山下
時間:2018-10-24
機器學習實戰(Machine Learning in Action,@author: Peter Harrington)
原始碼下載地址:https://www.manning.com/books/machine-learning-in-action
[email protected]:pbharrin/machinelearninginaction.git

*************************************************************
一、決策樹的原理及原始碼解析

檔案:trees.py,是ID3決策樹演算法的實現。程式碼中的主要方法:

createDataSet----建立例子中的資料集5行×3列,即五條資料,前兩列為特徵,最後一列為是否為魚類;lable為兩個特徵的含義'no_surfacing'(不浮出水是否可以生存),'flippers'(是否有腳蹼)

例子:五個海洋生物,兩個特徵:不浮出水是否可以生存;是否有腳蹼。利用決策樹將這些動物分成兩類。
資料要求:第一,資料必須是一種列表元素組成的列表,而且所有的列表元素都要具有相同的的資料長度;第二,資料的最後一列或則每個例項的最後一個元素是當前例項的類別標籤。

#夏農熵的計算
calcShannonEnt----計算給定資料集的夏農熵,熵是集合資訊的度量方式,公式

#按照資訊增益選取最好的特徵
chooseBestFeatureToSplit----選擇最好的資料集劃分方式,返回最好的特徵(按照該特徵分類的資訊增益最大)索引。原理:遍歷所有特徵,按照當前特徵將其劃分(splitDataSet)為多個數據集,然後求他們資訊熵的和。其中資訊增益是熵的減少或則無序減少程度,這裡是前原始資料集的資訊熵與分類後的資訊熵之差。最後選取資訊增益最大的特徵,返回對應的列索引

#資料集切分
splitDataSet----按照給定特徵(axis)的值(value)劃分資料集dataset
================================================================
#決策樹構建
createTree----決策樹構建程式碼,是一個遞迴函式

原理:得到原始資料集,然後基於最好的屬性值劃分資料集,由於特徵值可能多於兩個,因此可能存在大於兩個分支的資料集劃分。第一次劃分之後,資料將被向下傳遞到樹分支的下一個節點,在這個節點上,再次劃分資料。因此我們採用遞迴的原則處理資料集。遞迴結束的條件:遍歷完所有劃分資料集的屬性,或則每個分支下的所有例項都具有相同的類(任何到達葉節點的資料必然屬於葉節點的分類)。第一種情況,如果遍歷完所有的屬性,但當前節點類標籤不唯一時,通過多數表決的方法(majorityCnt)來確定葉節點的分類。

majorityCnt----多數表決的演算法實現返回對應的類別

createTree程式碼解析:

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]    #獲取資料最後一列,即類別標籤
    if classList.count(classList[0]) == len(classList): #類別標籤是否完全相同(都是第一個標籤)
        return classList[0]                             #完全相同,停止,返回對應標籤,葉節點
    if len(dataSet[0]) == 1:                       #
只剩一個標籤(遍歷一個刪除一個,遍歷完了) return majorityCnt(classList) #投票決定標籤,葉節點 bestFeat = chooseBestFeatureToSplit(dataSet) #選取最佳的特徵列索引 bestFeatLabel = labels[bestFeat] #獲得其標籤 myTree = {bestFeatLabel:{}} #構建節點(字典,key為最佳的標籤) del(labels[bestFeat]) #
刪除已採用標籤 featValues = [example[bestFeat] for example in dataSet] #獲取最佳特徵列的值 uniqueVals = set(featValues) #取得其不重複集合 for value in uniqueVals: #遍歷 subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels) #在上面節點上增加節點,利用該特徵值取值下的資料集,遞迴 return myTree
>>> import trees
>>> import treePlotter
>>> data,lable=trees.createDataSet()
>>> data
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> lable
['no surfacing', 'flippers']
>>> mytree=trees.createTree(data,lable)
>>> mytree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

================================================================
#利用構建的樹完成測試樣本的分類。
#原理:程式比較測試資料與決策樹上的數值,遞迴執行該過程直到進入葉子節點;最後將測試資料定義為葉子節點所屬的型別
classify----利用構建的樹完成測試樣本的分類

def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]          #獲取根節點的key,即採用的第一個特徵(只有一個)
    secondDict = inputTree[firstStr]        #獲取根節點的子節點(有多個)
    featIndex = featLabels.index(firstStr)  #獲取獲取第一個特徵的所在的列索引
    key = testVec[featIndex]                #取出測試資料對應特徵的值
    valueOfFeat = secondDict[key]           #取出樹第二層中key與測試資料相等的節點
    if isinstance(valueOfFeat, dict):       #判斷取出的節點是否是dict物件
        classLabel = classify(valueOfFeat, featLabels, testVec)   #是,說明還有子節點,繼續尋找
    else: classLabel = valueOfFeat                                #否,說明已經到達葉節點,返回其類別
    return classLabel

測試:

>>> import trees
>>> import treePlotter
>>> data,lable=trees.createDataSet()
>>> data
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> lable
['no surfacing', 'flippers']
>>> mytree=trees.createTree(data,lable)
>>> mytree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

>>> data,lable=trees.createDataSet()#重置lable
>>> trees.classify(mytree,lable,[0,0])#利用構建的樹對測試資料分類
'no'
>>>

#其他方法
storeTree----利用pickle將構建的樹物件序列化儲存到本地

grabTree----利用pickle載入儲存在本地的樹檔案,構建樹

trees.py原始碼(測試見後文)

'''
Created on Oct 12, 2010
Decision Tree Source Code for Machine Learning in Action Ch. 3
@author: Peter Harrington
'''
from math import log
import operator

def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing','flippers']
    #change to discrete values
    return dataSet, labels

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet: #the the number of unique elements and their occurance
        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) #log base 2
    return shannonEnt
    
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]     #chop out axis used for splitting
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet
    
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #the last column is used for the labels
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures):        #iterate over all the features
        featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
        uniqueVals = set(featList)       #get a set of unique values
        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     #calculate the info gain; ie reduction in entropy
        if (infoGain > bestInfoGain):       #compare this to the best gain so far
            bestInfoGain = infoGain         #if better than current best, set to best
            bestFeature = i
    return bestFeature                      #returns an integer

def majorityCnt(classList):
    classCount={}
    for vote in classList:
        if vote not in classCount.keys(): classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]
    if classList.count(classList[0]) == len(classList): 
        return classList[0]#stop splitting when all of the classes are equal
    if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
        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[:]       #copy all of labels, so trees don't mess up existing 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)
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

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)
    
View Code

****************************************************************
二、利用matplotlib實現tree視覺化

檔案:treePlotter.py,程式碼中的主要方法:

其中方法createPlot可以將傳給它的tree視覺化展現出來

plotMidText----連線中間文字
plotTree----構建樹
createPlot----主入口,將傳給它的tree視覺化展現出來

#獲取葉節點的數目和樹的層數
getNumLeafs----葉節點的數目
getTreeDepth----樹的層數

#建立測試資料
retrieveTree----定義了兩個樹

#使用文字註解繪製樹節點
plotNode----使用文字註解繪製樹節點

treePlotter.py原始碼(測試見後文)
*************************************************************
三、測試實現決策樹分類

>>> em=trees.calcShannonEnt(test_tree_data[0])#計算測試資料熵
>>> em
0.9709505944546686
>>>

>>> import trees
>>> import treePlotter
>>> test_tree_data=trees.createDataSet()#測試資料
>>> test_tree_data
([[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']], ['no surfacing', 'flippers'])
>>> mytree=trees.createTree(*test_tree_data)#構建決策樹
>>> mytree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> treePlotter.createPlot(mytree)#視覺化樹

(圖Figure_1-1.png)

>>> test_tree_data=trees.createDataSet()#注意上面資料已經改變了test_tree_data[1],即lable
>>> trees.classify(mytree,test_tree_data[1],[0,0])
'no'

--------------------------------------------------------------------------
#測試眼鏡的例子:特徵包括['age','prescript','astigmatic','tearRate'],類別包括['lenses','soft','hard']

>>> import trees
>>> import treePlotter
>>> fr=open('lenses.txt')
>>> lenses=[inst.strip().split('\t') for inst in fr.readlines()]
>>> lenseslable=['age','prescript','astigmatic','tearRate']
>>> lensestree=trees.createTree(lenses,lenseslable)
>>> lensestree
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses', 'presbyopic': 'no lenses', 'young': 'hard'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft'}}}}}}
>>> treePlotter.createPlot(lensestree)

(圖:測試眼)
存在問題:該樹的匹配選項過多,過度匹配

最後提醒,使用這裡的方法時,注意前文提到的資料要求