天天看點

[機器學習&資料挖掘]機器學習實戰決策樹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))      

繪制出來的圖形如下:

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

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

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

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

思路:這裡繪圖,作者選取了一個很聰明的方式,并不會因為樹的節點的增減和深度的增減而導緻繪制出來的圖形出現問題,當然不能太密集。這裡利用整棵樹的葉子節點數作為份數将整個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,即如同下圖:

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

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的坐标能能夠很好的将樹的葉子節點數和深度考慮進去,是以圖的邏輯比例就很好的确定了,這樣不用去關心輸出圖形的大小,一旦圖形發生變化,函數會重新繪制,但是假如利用像素為機關來繪制圖形,這樣縮放圖形就比較有難度了

繼續閱讀