[機器學習&數據挖掘]機器學習實戰決策樹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的坐標能能夠很好的將樹的葉子節點數和深度考慮進去,因此圖的邏輯比例就很好的確定了,這樣不用去關心輸出圖形的大小,一旦圖形發生變化,函數會重新繪制,但是假如利用像素為單位來繪制圖形,這樣縮放圖形就比較有難度了