摘要:本文首先淺談了自己對決策樹的理解,進而通過Python一步步構造決策樹,並通過Matplotlib更直觀的繪制樹形圖,最后,選取相應的數據集對算法進行測試。
最近在看《機器學習實戰》這本書,因為一直想好好了解機器學習方面的算法,加之想學Python,就在朋友的推薦之下選擇了這本同等定位的書。今天就來學習一下決策樹,所有的代碼均python3.4實現,確實與2.7有很多不同。
決策樹和KNN一樣,都是處理分類問題的算法。對於決策樹的定義不計其數,就我個人而言,首先單看名字,就想到了最小生成樹,猜想圖解的話這個算法會是一棵樹,在機器學習這個層面,將所要處理的數據看做是樹的根,相應的選取數據的特征作為一個個節點(決策點),每次選取一個節點將數據集分為不同的數據子集,可以看成對樹進行分支,這里體現出了決策,直到最后無法可分停止,也就是分支上的數據為同一類型,可以想象一次次划分之后由根延伸出了許多分支,形象的說就是一棵樹。
在機器學習中,決策樹是一個預測模型,它代表的是對象屬性與對象值之間的一種映射關系,我們可以利用決策樹發現數據內部所蘊含的知識,比如在本文的最后我們選取隱形眼鏡數據集根據決策樹學習到眼科醫生是如何判斷患者佩戴眼鏡片的過程,而K近鄰算法雖與決策樹同屬分類,卻無從得知數據的內在形式。下面我們就一步步的學習決策樹:
1. 構造決策樹
基於之前的了解,在構造決策樹首先需要選取特征將原始數據划分為幾個數據集,那么第一個問題就是當前數據的哪個特征在划分數據分類時起決定性作用,所以必須評估每個特征。進而通過特征將原始數據就被划分為幾個數據子集,這些數據子集分布在第一個決策點的所有分支上,如果分支上的所有數據為同一類型,則划分停止,若分支上的所有數據不是同一類型,則還需要繼續划分,直到所有具有相同類型的數據均在一個數據子集中。在用決策樹進行划分時,關鍵是每次划分時選取哪個特征進行划分,在划分數據時,我們必須采用量化的方法判斷如何划分數據。
(1)信息增益
划分數據時是根據某一原則進行划分,使得划分在同一集合中的數據具有共同的特征,據此,我們可以理解為划分數據的原則就是是無序的數據變得有序。當然划分數據有很多種方法,在此選用信息論度量信息,划分組織雜亂無章的數據。
信息論是量化處理信息的分支科學,可以在數據划分之前或之后使用信息論量化度量信息的內容。其中在划分數據集之前之后信息發生的變化稱為信息增益,計算每個特征值划分數據集獲得的信息增益,獲得信息增益最高的特征就是最好的選擇。
首先我們需要知道怎么計算信息增益,集合信息的度量方式稱為香農熵或者簡稱為熵,熵定義為信息的期望值,那么信息是什么?xi的信息可定義為:L(xi) = -log(p(xi)),其中p(xi)是選擇該分類的概率。
熵指的是所有類別所有可能值包含的信息期望值,可表示為:
熵越高,表明混合的數據越多,則可以在數據集中添加更多的分類。基於上述的分析,編程計算給定數據集的香農熵,代碼如下:
from math import log ###計算香農熵(為float類型) def calShang(dataSet): numEntries = len(dataSet) labelCounts = {}##創建字典 for featVec in dataSet: currentLabel = featVec[-1] if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 shannonEnt = 0.0 for key in labelCounts: prob = float(labelCounts[key]) / numEntries shannonEnt -= prob * log(prob,2) return shannonEnt
對此我們可以輸入數據集測試:
def creatDataSet(): dataSet = [[1,1,'yes'], [1,1,'yes'], [1,0,'no'], [0,1,'no'], [0,1,'no']] labels = ['no surfacing','flippers'] return dataSet,labels ''' #測試 myData,labels = creatDataSet() print("原數據為:",myData) print("標簽為:",labels) shang = calShang(myData) print("香農熵為:",shang) '''
得到熵之后,我們就可以按照獲取最大增益的辦法划分數據集。
(2)划分數據集
基於之前的分析,信息增益表示的是信息的變化,而信息可以用熵來度量,所以我們可以用熵的變化來表示信息增益。而獲得最高信息增益的特征就是最好的選擇,故此,我們可以對所有特征遍歷,得到最高信息增益的特征加以選擇。
首先,我們按照給定特征划分數據集並進行簡單的測試:
###划分數據集(以指定特征將數據進行划分) def splitDataSet(dataSet,feature,value):##傳入待划分的數據集、划分數據集的特征以及需要返回的特征的值 newDataSet = [] for featVec in dataSet: if featVec[feature] == value: reducedFeatVec = featVec[:feature] reducedFeatVec.extend(featVec[feature + 1:]) newDataSet.append(reducedFeatVec) return newDataSet ''' #測試 myData,labels = creatDataSet() print("原數據為:",myData) print("標簽為:",labels) split = splitDataSet(myData,0,1) print("划分后的結果為:",split) '''
接下來我們遍歷整個數據集,循環計算香農熵和splitDataSet()函數,找到最好的划分方式並簡單測試:
##選擇最好的划分方式(選取每個特征划分數據集,從中選取信息增益最大的作為最優划分)在這里體現了信息增益的概念 def chooseBest(dataSet): featNum = len(dataSet[0]) - 1 baseEntropy = calShang(dataSet) bestInforGain = 0.0 bestFeat = -1##表示最好划分特征的下標 for i in range(featNum): featList = [example[i] for example in dataSet] #列表 uniqueFeat = set(featList)##得到每個特征中所含的不同元素 newEntropy = 0.0 for value in uniqueFeat: subDataSet = splitDataSet(dataSet,i,value) prob = len(subDataSet) / len(dataSet) newEntropy += prob * calShang(subDataSet) inforGain = baseEntropy - newEntropy if (inforGain > bestInforGain): bestInforGain = inforGain bestFeature = i#第i個特征是最有利於划分的特征 return bestFeature ''' ##測試 myData,labels = creatDataSet() best = chooseBest(myData) print(best) '''
(3)遞歸構建決策樹
基於之前的分析,我們選取划分結果最好的特征划分數據集,由於特征很可能多與兩個,因此可能存在大於兩個分支的數據集划分,第一次划分之后,可以將划分的數據繼續向下傳遞,如果將每一個划分的數據看成是原數據集,那么之后的每一次划分都可以看成是和第一次划分相同的過程,據此我們可以采用遞歸的原則處理數據集。遞歸結束的條件是:程序遍歷完所有划分數據集的屬性,或者每個分支下的所有實例都有相同的分類。編程實現:
##遞歸構建決策樹 import operator #返回出現次數最多的分類名稱 def majorClass(classList): classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] += 1 #降序排序,可以指定reverse = true sortedClassCount = sorted(classcount.iteritems(),key = operator.itemgetter(1),reverse = true) return sortedClassCount[0][0] #創建樹 def creatTree(dataSet,labels): classList = [example[-1] for example in dataSet] if classList.count(classList[0]) == len(classList): return classList[0] if len(dataSet[0]) == 1: return majorClass(classList) bestFeat = chooseBest(dataSet) bestFeatLabel = labels[bestFeat] myTree = {bestFeatLabel:{}} del(labels[bestFeat]) featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: subLabels = labels[:] myTree[bestFeatLabel][value] = creatTree(splitDataSet(dataSet,bestFeat,value),subLabels) return myTree ''' #測試 myData,labels = creatDataSet() mytree = creatTree(myData,labels) print(mytree) '''
2.使用matplotlib注解繪制樹形圖
之前我們已經從數據集中成功的創建了決策樹,但是字典的形式非常的不易於理解,因此本節采用Matplotlib庫創建樹形圖。
首先,使用文本注解繪制樹節點:
##采用matplotlib繪制樹形圖 import matplotlib.pyplot as plt decisionNode = dict(boxstyle="sawtooth", fc="0.8") leafNode = dict(boxstyle="round4", fc="0.8") arrow_args = dict(arrowstyle="<-") #繪制樹節點 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 getNumLeafs(myTree): numLeafs = 0 #firstStr = myTree.keys()[0] firstSides = list(myTree.keys()) firstStr = firstSides[0]#找到輸入的第一個元素 secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]) == dict: numLeafs += getNumLeafs(secondDict[key]) else: numLeafs += 1 return numLeafs def getTreeDepth(myTree): maxDepth = 1 firstSides = list(myTree.keys()) firstStr = firstSides[0]#找到輸入的第一個元素 #firstStr = myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]) == dict: thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth def retrieveTree(i): listOfTrees =[{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}, {'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}} ] return listOfTrees[i] #測試 mytree = retrieveTree(0) print(getNumLeafs(mytree)) print(getTreeDepth(mytree))
在此,我們說明一下Python2.7和3.4在實現本段代碼的區別:
在2.7中,找到key所對應的第一個元素為:firstStr = myTree.keys()[0],這在3.4中運行會報錯:'dict_keys' object does not support indexing,這是因為python3改變了dict.keys,返回的是dict_keys對象,支持iterable 但不支持indexable,我們可以將其明確的轉化成list,則此項功能在3中應這樣實現:
firstSides = list(myTree.keys()) firstStr = firstSides[0]#找到輸入的第一個元素
繪制樹:
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): numLeafs = getNumLeafs(myTree) depth = getTreeDepth(myTree) firstSides = list(myTree.keys()) firstStr = firstSides[0]#找到輸入的第一個元素 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': plotTree(secondDict[key],cntrPt,str(key)) else: 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 def createPlot(inTree): fig = plt.figure(1, facecolor='white') fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) 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() #測試 mytree = retrieveTree(0) print(mytree) createPlot(mytree)
測試之后結果如下:
這樣相比於字典形式確實清晰了很多。
3.測試算法
在本章中,我們首先使用決策樹對實際數據進行分類,然后使用決策樹預測隱形眼鏡類型對算法進行驗證。
(1)使用決策樹執行分類
在使用了訓練數據構造了決策樹之后,我們便可以將它用於實際數據的分類:
###決策樹的分類函數,返回當前節點的分類標簽 def classify(inputTree,featLabels,testVec):##傳入的數據為dict類型 firstSides = list(inputTree.keys()) firstStr = firstSides[0]#找到輸入的第一個元素 ##這里表明了python3和python2版本的差別,上述兩行代碼在2.7中為:firstStr = inputTree.key()[0] secondDict = inputTree[firstStr]##建一個dict #print(secondDict) featIndex = featLabels.index(firstStr)#找到在label中firstStr的下標 for i in secondDict.keys(): print(i) for key in secondDict.keys(): if testVec[featIndex] == key: if type(secondDict[key]) == dict:###判斷一個變量是否為dict,直接type就好 classLabel = classify(secondDict[key],featLabels,testVec) else: classLabel = secondDict[key] return classLabel ##比較測試數據中的值和樹上的值,最后得到節點 #測試 myData,labels = creatDataSet() print(labels) mytree = retrieveTree(0) print(mytree) classify = classify(mytree,labels,[1,0]) print(classify)
(2)使用決策樹預測隱形眼鏡類型
基於之前的分析,我們知道可以根據決策樹學習到眼科醫生是如何判斷患者需要佩戴的眼鏡片,據此我們可以幫助人們判斷需要佩戴的鏡片類型。
在此從UCI數據庫中選取隱形眼鏡數據集lenses.txt,它包含了很多患者眼部狀況的觀察條件以及醫生推薦的隱形眼鏡類型。我們選取此數據集,結合Matplotlib繪制樹形圖,進一步觀察決策樹是如何工作的,具體的代碼如下:
fr = open('lenses.txt') lenses = [inst.strip().split('\t') for inst in fr.readlines()] lensesLabels = ['ages','prescript','astigmatic','tearRate'] lensesTree = creatTree(lenses,lensesLabels) print(lensesTree) createPlot(lensesTree)
得到的樹形圖:
沿着決策樹的不同分支,我們可以得到不同患者需要佩戴的隱形眼鏡類型,從該圖中我們可以得到,只需要問四個問題就可以確定出患者需要佩戴何種隱形眼鏡。
本章主要使用的是ID3算法,自身也存在着很多不足。當然還有其它的決策樹構造算法,比如C4.5和CART,以后有機會了再好好看看。
以上是我自己的一些理解與總結,難免有錯,望大家不吝指教~