[機器學習&資料探勘]機器學習實戰決策樹plotTree函式完全解析

風痕影默發表於2015-06-23

在看機器學習實戰時候,到第三章的對決策樹畫圖的時候,有一段遞迴函式怎麼都看不懂,因為以後想選這個方向為自己的職業導向,抱著精看的態度,對這本樹進行地毯式掃描,所以就沒跳過,一直卡了一天多,才差不多搞懂,才對那個函式中的plotTree.xOff的取值,以及計算cntrPt的方法搞懂,相信也有人和我一樣,希望能夠相互交流。

先把程式碼貼在這裡:

import matplotlib.pyplot as plt

#這裡是對繪製是圖形屬性的一些定義,可以不用管,主要是後面的演算法
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")

#這是遞迴計算樹的葉子節點個數,比較簡單
def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            numLeafs += getNumLeafs(secondDict[key])
        else:   numLeafs +=1
    return numLeafs
#這是遞迴計算樹的深度,比較簡單
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth
#這個是用來一註釋形式繪製節點和箭頭線,可以不用管
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 plotMidText(cntrPt, parentPt, txtString):
    xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
    createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)
#重點,遞迴,決定整個樹圖的繪製,難(自己認為)
def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]     #the text label for this node should be this
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
    
    plotMidText(cntrPt, parentPt, nodeTxt)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes   
            plotTree(secondDict[key],cntrPt,str(key))        #recursion
        else:   #it's a leaf node print the leaf node
            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.0/plotTree.totalD
#if you do get a dictonary you know it's a tree, and the first element will be another dict

#這個是真正的繪製,上邊是邏輯的繪製
def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False)    #no ticks
    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()

#這個是用來建立資料集即決策樹
def retrieveTree(i):
    listOfTrees =[{'no surfacing': {0:{'flippers': {0: 'no', 1: 'yes'}}, 1: {'flippers': {0: 'no', 1: 'yes'}}, 2:{'flippers': {0: 'no', 1: 'yes'}}}},
                  {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
                  ]
    return listOfTrees[i]

createPlot(retrieveTree(0))

繪製出來的圖形如下:

先導:這裡說一下為什麼說一個遞迴樹的繪製為什麼會是很難懂,這裡不就是利用遞迴函式來繪圖麼,就如遞迴計算樹的深度、葉子節點一樣,問題不是遞迴的思路,而是這本書中一些座標的起始取值、以及在計算節點座標所作的處理,而且在樹中對這部分並沒有取講述,所以在看這段程式碼的時候可能大體思路明白但是具體的細節卻知之甚少,所以本篇主要是對其中書中提及甚少的作詳細的講述,當然程式碼的整體思路也不會放過的

準備:這裡說一下具體繪製的時候是利用自定義plotNode函式來繪製,這個函式一次繪製的是一個箭頭和一個節點,如下圖:

思路:這裡繪圖,作者選取了一個很聰明的方式,並不會因為樹的節點的增減和深度的增減而導致繪製出來的圖形出現問題,當然不能太密集。這裡利用整棵樹的葉子節點數作為份數將整個x軸的長度進行平均切分,利用樹的深度作為份數將y軸長度作平均切分,並利用plotTree.xOff作為最近繪製的一個葉子節點的x座標,當再一次繪製葉子節點座標的時候才會plotTree.xOff才會發生改變;用plotTree.yOff作為當前繪製的深度,plotTree.yOff是在每遞迴一層就會減一份(上邊所說的按份平均切分),其他時候是利用這兩個座標點去計算非葉子節點,這兩個引數其實就可以確定一個點座標,這個座標確定的時候就是繪製節點的時候

整體演算法的遞迴思路倒是很容易理解:

每一次都分三個步驟:

  (1)繪製自身

  (2)判斷子節點非葉子節點,遞迴

  (3)判斷子節點為葉子節點,繪製

詳細解析:

def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #this determines the x width of this tree
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]     #the text label for this node should be this
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
    
    plotMidText(cntrPt, parentPt, nodeTxt)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes   
            plotTree(secondDict[key],cntrPt,str(key))        #recursion
        else:   #it's a leaf node print the leaf node
            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.0/plotTree.totalD
#if you do get a dictonary you know it's a tree, and the first element will be another dict

def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False)    #no ticks
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;#totalW為整樹的葉子節點樹,totalD為深度
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

上邊程式碼中紅色部分如此處理原理:

首先由於整個畫布根據葉子節點數和深度進行平均切分,並且x軸的總長度為1,即如同下圖:

1、其中方形為非葉子節點的位置,@是葉子節點的位置,因此每份即上圖的一個表格的長度應該為1/plotTree.totalW,但是葉子節點的位置應該為@所在位置,則在開始的時候plotTree.xOff的賦值為-0.5/plotTree.totalW,即意為開始x位置為第一個表格左邊的半個表格距離位置,這樣作的好處為:在以後確定@位置時候可以直接加整數倍的1/plotTree.totalW,

2、對於plotTree函式中的紅色部分即如下:

cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)

plotTree.xOff即為最近繪製的一個葉子節點的x座標,在確定當前節點位置時每次只需確定當前節點有幾個葉子節點,因此其葉子節點所佔的總距離就確定了即為float(numLeafs)/plotTree.totalW*1(因為總長度為1),因此當前節點的位置即為其所有葉子節點所佔距離的中間即一半為float(numLeafs)/2.0/plotTree.totalW*1,但是由於開始plotTree.xOff賦值並非從0開始,而是左移了半個表格,因此還需加上半個表格距離即為1/2/plotTree.totalW*1,則加起來便為(1.0 + float(numLeafs))/2.0/plotTree.totalW*1,因此偏移量確定,則x位置變為plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW

3、對於plotTree函式引數賦值為(0.5, 1.0)

因為開始的根節點並不用劃線,因此父節點和當前節點的位置需要重合,利用2中的確定當前節點的位置便為(0.5, 1.0)

總結:利用這樣的逐漸增加x的座標,以及逐漸降低y的座標能能夠很好的將樹的葉子節點數和深度考慮進去,因此圖的邏輯比例就很好的確定了,這樣不用去關心輸出圖形的大小,一旦圖形發生變化,函式會重新繪製,但是假如利用畫素為單位來繪製圖形,這樣縮放圖形就比較有難度了

相關文章