[機器學習&數據挖掘]機器學習實戰決策樹plotTree函數完全解析


在看機器學習實戰時候,到第三章的對決策樹畫圖的時候,有一段遞歸函數怎么都看不懂,因為以后想選這個方向為自己的職業導向,抱着精看的態度,對這本樹進行地毯式掃描,所以就沒跳過,一直卡了一天多,才差不多搞懂,才對那個函數中的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的坐標能能夠很好的將樹的葉子節點數和深度考慮進去,因此圖的邏輯比例就很好的確定了,這樣不用去關心輸出圖形的大小,一旦圖形發生變化,函數會重新繪制,但是假如利用像素為單位來繪制圖形,這樣縮放圖形就比較有難度了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM