python機器學習實戰(二)
版權聲明:本文為博主原創文章,轉載請指明轉載地址
http://www.cnblogs.com/fydeblog/p/7159775.html
前言
這篇notebook是關於機器學習監督學習中的決策樹算法,內容包括決策樹算法的構造過程,使用matplotlib庫繪制樹形圖以及使用決策樹預測隱形眼睛類型.
操作系統:ubuntu14.04(win也ok) 運行環境:anaconda-python2.7-jupyter notebook 參考書籍:機器學習實戰和源碼 notebook writer ----方陽
注意事項:在這里說一句,默認環境python2.7的notebook,用python3.6的會出問題,還有我的目錄可能跟你們的不一樣,你們自己跑的時候記得改目錄,我會把notebook和代碼以及數據集放到結尾的百度雲盤,方便你們下載!
決策樹原理:不斷通過數據集的特征來划分數據集,直到遍歷所有划分數據集的屬性,或每個分支下的實例都具有相同的分類,決策樹算法停止運行。
決策樹的優缺點及適用類型
優點 :計算復雜度不高, 輸出結果易於理解,對中間值的缺失不敏感,可以處理不相關特征數據。
缺點 :可能會產生過度匹配問題。
適用數據類型:數值型和標稱型
先舉一個小例子,讓你了解決策樹是干嘛的,簡單來說,決策樹算法就是一種基於特征的分類器,拿郵件來說吧,試想一下,郵件的類型有很多種,有需要及時處理的郵件,無聊是觀看的郵件,垃圾郵件等等,我們需要去區分這些,比如根據郵件中出現里你的名字還有你朋友的名字,這些特征就會就可以將郵件分成兩類,需要及時處理的郵件和其他郵件,這時候在分類其他郵件,例如郵件中出現buy,money等特征,說明這是垃圾推廣文件,又可以將其他文件分成無聊是觀看的郵件和垃圾郵件了。
1.決策樹的構造
1.1 信息增益
試想一下,一個數據集是有多個特征的,我們該從那個特征開始划分呢,什么樣的划分方式會是最好的?
我們知道划分數據集的大原則是將無序的數據變得更加有序,這樣才能分類得更加清楚,這里就提出了一種概念,叫做信息增益,它的定義是在划分數據集之前之后信息發生的變化,變化越大,證明划分得越好,所以在划分數據集的時候,獲得增益最高的特征就是最好的選擇。
這里又會扯到另一個概念,信息論中的熵,它是集合信息的度量方式,熵變化越大,信息增益也就越大。信息增益是熵的減少或者是數據無序度的減少.
一個符號x在信息論中的信息定義是 l(x)= -log(p(x)) ,這里都是以2為底,不再復述。
則熵的計算公式是 H =-∑p(xi)log(p(xi)) (i=1,2,..n)
下面開始實現給定數據集,計算熵
參考代碼:
1 from math import log #we use log function to calculate the entropy 2 import operator
1 def calcShannonEnt(dataSet): 2 numEntries = len(dataSet) 3 labelCounts = {} 4 for featVec in dataSet: #the the number of unique elements and their occurance 5 currentLabel = featVec[-1] 6 if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0 7 labelCounts[currentLabel] += 1 8 shannonEnt = 0.0 9 for key in labelCounts: 10 prob = float(labelCounts[key])/numEntries 11 shannonEnt -= prob * log(prob,2) #log base 2 12 return shannonEnt
程序思路: 首先計算數據集中實例的總數,由於代碼中多次用到這個值,為了提高代碼效率,我們顯式地聲明一個變量保存實例總數. 然后 ,創建一個數據字典labelCounts,它的鍵值是最后一列(分類的結果)的數值.如果當前鍵值不存在,則擴展字典並將當前鍵值加入字典。每個鍵值都記錄了當前類別出現的次數。 最后 , 使用所有類標簽的發生頻率計算類別出現的概率。我們將用這個概率計算香農熵。
讓我們來測試一下,先自己定義一個數據集
下表的數據包含 5 個海洋動物,特征包括:不浮出水面是否可以生存,以及是否有腳蹼。我們可以將這些動物分成兩類: 魚類和非魚類。
根據上面的表格,我們可以定義一個createDataSet函數
參考代碼如下
1 def createDataSet(): 2 dataSet = [[1, 1, 'yes'], 3 [1, 1, 'yes'], 4 [1, 0, 'no'], 5 [0, 1, 'no'], 6 [0, 1, 'no']] 7 labels = ['no surfacing','flippers'] 8 #change to discrete values 9 return dataSet, labels
把所有的代碼都放在trees.py中(以下在jupyter)
cd /home/fangyang/桌面/machinelearninginaction/Ch03
/home/fangyang/桌面/machinelearninginaction/Ch03
import trees
myDat, labels = trees.createDataSet()
myDat #old data set
現在來測試測試
import trees myDat, labels = trees.createDataSet() trees.chooseBestFeatureToSplit(myDat) #return the index of best character to split
0
1.3 遞歸構建決策樹
好了,到現在,我們已經知道如何基於最好的屬性值去划分數據集了,現在進行下一步,如何去構造決策樹
決策樹的實現原理:得到原始數據集, 然后基於最好的屬性值划分數據集,由於特征值可能多於兩個,因此可能存在大於兩個分支的數據集划分。第一次划分之后, 數據將被向下傳遞到樹分支的下一個節點, 在這個節點上 ,我們可以再次划分數據。因此我們可以采用遞歸的原則處理數據集。
遞歸結束的條件是:程序遍歷完所有划分數據集的屬性, 或者每個分支下的所有實例都具有相同的分類。
這里先構造一個majorityCnt函數,它的作用是返回出現次數最多的分類名稱,后面會用到
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]
這個函數在實戰一中的一個函數是一樣的,復述一遍,classCount定義為存儲字典,每當,由於后面加了1,所以每次出現鍵值就加1,就可以就算出鍵值出現的次數里。最后通過sorted函數將classCount字典分解為列表,sorted函數的第二個參數導入了運算符模塊的itemgetter方法,按照第二個元素的次序(即數字)進行排序,由於此處reverse=True,是逆序,所以按照從大到小的次序排列。
讓我們來測試一下
import numpy as np classList = np.array(myDat).T[-1]
classList
array(['yes', 'yes', 'no', 'no', 'no'], dtype='|S21')
majorityCnt(classList) #the number of 'no' is 3, 'yes' is 2,so return 'no'
1 def createTree(dataSet,labels): 2 classList = [example[-1] for example in dataSet] 3 if classList.count(classList[0]) == len(classList): 4 return classList[0]#stop splitting when all of the classes are equal 5 if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet 6 return majorityCnt(classList) 7 bestFeat = chooseBestFeatureToSplit(dataSet) 8 bestFeatLabel = labels[bestFeat] 9 myTree = {bestFeatLabel:{}} 10 del(labels[bestFeat]) #delete the best feature , so it can find the next best feature 11 featValues = [example[bestFeat] for example in dataSet] 12 uniqueVals = set(featValues) 13 for value in uniqueVals: 14 subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels 15 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels) 16 return myTree
前面兩個if語句是判斷分類是否結束,當所有的類都相等時,也就是屬於同一類時,結束再分類,又或特征全部已經分類完成了,只剩下最后的class,也結束分類。這是判斷遞歸結束的兩個條件。一般開始的時候是不會運行這兩步的,先選最好的特征,使用 chooseBestFeatureToSplit函數得到最好的特征,然后進行分類,這里創建了一個大字典myTree,它將決策樹的整個架構全包含進去,這個等會在測試的時候說,然后對數據集進行划分,用splitDataSet函數,就可以得到划分后新的數據集,然后再進行createTrees函數,直到遞歸結束。
來測試一下
myTree = trees.createTree(myDat,labels)
myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
再來說說上面沒詳細說明的大字典,myTree是特征是‘no surfacing’,根據這個分類,得到兩個分支‘0’和‘1‘,‘0’分支由於全是同一類就遞歸結束里,‘1’分支不滿足遞歸結束條件,繼續進行分類,它又會生成它自己的字典,又會分成兩個分支,並且這兩個分支滿足遞歸結束的條件,所以返回‘no surfacing’上的‘1’分支是一個字典。這種嵌套的字典正是決策樹算法的結果,我們可以使用它和Matplotlib來進行畫決策
1.4 使用決策樹執行分類
這個就是將測試合成一個函數,定義為classify函數
參考代碼如下:
1 def classify(inputTree,featLabels,testVec): 2 firstStr = inputTree.keys()[0] 3 secondDict = inputTree[firstStr] 4 featIndex = featLabels.index(firstStr) 5 key = testVec[featIndex] 6 valueOfFeat = secondDict[key] 7 if isinstance(valueOfFeat, dict): 8 classLabel = classify(valueOfFeat, featLabels, testVec) 9 else: classLabel = valueOfFeat 10 return classLabel
這個函數就是一個根據決策樹來判斷新的測試向量是那種類型,這也是一個遞歸函數,拿上面決策樹的結果來說吧。
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},這是就是我們的inputTree,首先通過函數的第一句話得到它的第一個bestFeat,也就是‘no surfacing’,賦給了firstStr,secondDict就是‘no surfacing’的值,也就是 {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}},然后用index函數找到firstStr的標號,結果應該是0,根據下標,把測試向量的值賦給key,然后找到對應secondDict中的值,這里有一個isinstance函數,功能是第一個參數的類型等於后面參數的類型,則返回true,否則返回false,testVec列表第一位是1,則valueOfFeat的值是 {0: 'no', 1: 'yes'},是dict,則遞歸調用這個函數,再進行classify,知道不是字典,也就最后的結果了,其實就是將決策樹過一遍,找到對應的labels罷了。
這里有一個小知識點,在jupyter notebook中,顯示綠色的函數,可以通過下面查詢它的功能,例如
isinstance? #run it , you will see a below window which is used to introduce this function
讓我們來測試測試
trees.classify(myTree,labels,[1,0])
‘no’
trees.classify(myTree,labels,[1,1])
‘yes'
1.5 決策樹的存儲
構造決策樹是很耗時的任務,即使處理很小的數據集, 如前面的樣本數據, 也要花費幾秒的時間 ,如果數據集很大,將會耗費很多計算時間。然而用創建好的決策樹解決分類問題,可以很快完成。因此 ,為了節省計算時間,最好能夠在每次執行分類時調用巳經構造好的決策樹。
解決方案:使用pickle模塊存儲決策樹
參考代碼:
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)
就是將決策樹寫到文件中,用的時候在取出來,測試一下就明白了
trees.storeTree(myTree,'classifierStorage.txt') #run it ,store the tree
trees.grabTree('classifierStorage.txt')
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
決策樹的構造部分結束了,下面介紹怎樣繪制決策樹
2. 使用Matplotlib注解繪制樹形圖
前面我們看到決策樹最后輸出是一個大字典,非常丑陋,我們想讓它更有層次感,更加清晰,最好是圖形狀的,於是,我們要Matplotlib去畫決策樹。
2.1 Matplotlib注解
Matplotlib提供了一個注解工具annotations,它可以在數據圖形上添加文本注釋。
創建一個treePlotter.py文件來存儲畫圖的相關函數
首先是使用文本注解繪制樹節點,參考代碼如下:
1 import matplotlib.pyplot as plt 2 3 decisionNode = dict(boxstyle="sawtooth", fc="0.8") 4 leafNode = dict(boxstyle="round4", fc="0.8") 5 arrow_args = dict(arrowstyle="<-") 6 7 def plotNode(nodeTxt, centerPt, parentPt, nodeType): 8 createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',\ 9 xytext=centerPt, textcoords='axes fraction',\ 10 va="center", ha="center", bbox=nodeType, arrowprops=arrow_args ) 11 12 def createPlot1(): 13 fig = plt.figure(1, facecolor='white') 14 fig.clf() 15 createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 16 plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode) 17 plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode) 18 plt.show()
前面三行是定義文本框和箭頭格式,decisionNode是鋸齒形方框,文本框的大小是0.8,leafNode是4邊環繞型,跟矩形類似,大小也是4,arrow_args是指箭頭,我們在后面結果是會看到這些東西,這些數據以字典類型存儲。第一個plotNode函數的功能是繪制帶箭頭的注解,輸入參數分別是文本框的內容,文本框的中心坐標,父結點坐標和文本框的類型,這些都是通過一個createPlot.ax1.annotate函數實現的,create.ax1是一個全局變量,這個函數不多將,會用就行了。第二個函數createPlot就是生出圖形,也沒什么東西,函數第一行是生成圖像的畫框,橫縱坐標最大值都是1,顏色是白色,下一個是清屏,下一個就是分圖,111中第一個1是行數,第二個是列數,第三個是第幾個圖,這里就一個圖,跟matlab中的一樣,matplotlib里面的函數都是和matlab差不多。
來測試一下吧
reset -f #clear all the module and data
cd 桌面/machinelearninginaction/Ch03
/home/fangyang/桌面/machinelearninginaction/Ch03
import treePlotter import matplotlib.pyplot as plt
treePlotter.createPlot1()
2.2 構造注解樹
繪制一棵完整的樹需要一些技巧。我們雖然有 x 、y 坐標,但是如何放置所有的樹節點卻是個問題,我們必須知道有多少個葉節點,以便可以正確確定x軸的長度;我們還需要知道樹有多少層,以便可以正確確定y軸的高度。這里定義了兩個新函數getNumLeafs()和getTreeDepth(),以求葉節點的數目和樹的層數。
參考代碼:
1 def getNumLeafs(myTree): 2 numLeafs = 0 3 firstStr = myTree.keys()[0] 4 secondDict = myTree[firstStr] 5 for key in secondDict.keys(): 6 if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes 7 numLeafs += getNumLeafs(secondDict[key]) 8 else: numLeafs +=1 9 return numLeafs 10 11 def getTreeDepth(myTree): 12 maxDepth = 0 13 firstStr = myTree.keys()[0] 14 secondDict = myTree[firstStr] 15 for key in secondDict.keys(): 16 if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes 17 thisDepth = 1 + getTreeDepth(secondDict[key]) 18 else: thisDepth = 1 19 if thisDepth > maxDepth: maxDepth = thisDepth 20 return maxDepth
我們可以看到兩個方法有點似曾相識,沒錯,我們在進行決策樹分類測試時,用的跟這個幾乎一樣,分類測試中的isinstance函數換了一種方式去判斷,遞歸依然在,不過是每遞歸依次,高度增加1,葉子數同樣是檢測是否為字典,不是字典則增加相應的分支。
這里還寫了一個函數retrieveTree,它的作用是預先存儲的樹信息,避免了每次測試代碼時都要從數據中創建樹的麻煩
參考代碼如下
1 def retrieveTree(i): 2 listOfTrees =[{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}, 3 {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}} 4 ] 5 return listOfTrees[i]
這個沒什么好說的,就是把決策樹的結果存在一個函數中,方便調用,跟前面的存儲決策樹差不多。
有了前面這些基礎后,我們就可以來畫樹了。
參考代碼如下:
1 def plotMidText(cntrPt, parentPt, txtString): 2 xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] 3 yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1] 4 createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30) 5 6 def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on 7 numLeafs = getNumLeafs(myTree) #this determines the x width of this tree 8 depth = getTreeDepth(myTree) 9 firstStr = myTree.keys()[0] #the text label for this node should be this 10 cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) 11 plotMidText(cntrPt, parentPt, nodeTxt) 12 plotNode(firstStr, cntrPt, parentPt, decisionNode) 13 secondDict = myTree[firstStr] 14 plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD 15 for key in secondDict.keys(): 16 if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes 17 plotTree(secondDict[key],cntrPt,str(key)) #recursion 18 else: #it's a leaf node print the leaf node 19 plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW 20 plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) 21 plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) 22 plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD 23 #if you do get a dictonary you know it's a tree, and the first element will be another dict 24 25 def createPlot(inTree): 26 fig = plt.figure(1, facecolor='white') 27 fig.clf() 28 axprops = dict(xticks=[], yticks=[]) 29 createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) 30 plotTree.totalW = float(getNumLeafs(inTree)) 31 plotTree.totalD = float(getTreeDepth(inTree)) 32 plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; 33 plotTree(inTree, (0.5,1.0), '') 34 plt.show()
第一個函數是在父子節點中填充文本信息,函數中是將父子節點的橫縱坐標相加除以2,上面寫得有一點點不一樣,但原理是一樣的,然后還是在這個中間坐標的基礎上添加文本,還是用的是 createPlot.ax1這個全局變量,使用它的成員函數text來添加文本,里面是它的一些參數。
第二個函數是關鍵,它調用前面我們說過的函數,用樹的寬度用於計算放置判斷節點的位置 ,主要的計算原則是將它放在所有葉子節點的中間,而不僅僅是它子節點的中間,根據高度就可以平分坐標系了,用坐標系的最大值除以高度,就是每層的高度。這個plotTree函數也是個遞歸函數,每次都是調用,畫出一層,知道所有的分支都不是字典后,才算畫完。每次檢測出是葉子,就記錄下它的坐標,並寫出葉子的信息和父子節點間的信息。plotTree.xOff和plotTree.yOff是用來追蹤已經繪制的節點位置,以及放置下一個節點的恰當位置。
第三個函數我們之前介紹介紹過一個類似,這個函數調用了plotTree函數,最后輸出樹狀圖,這里只說兩點,一點是全局變量plotTree.totalW存儲樹的寬度 ,全 局變量plotTree.totalD存儲樹的深度,還有一點是plotTree.xOff和plotTree.yOff是在這個函數這里初始化的。
最后我們來測試一下
cd 桌面/machinelearninginaction/Ch03