機器學習演算法--決策樹2
以機器學習實戰決策樹為例,實現具體的決策樹演算法:
1.資訊增益的實現
2.劃分資料集
3.遞迴構建決策樹
4.使用matplotlib構造決策樹
5.測試和儲存決策樹
6.例項--隱形眼鏡型別
1.資訊增益的實現
集合D中類別數y,各種類別概率為pk,則集合D的資訊熵為Ent(D)
屬性a的取值有a1,a2...av,取值為av的樣本集合為Dv,則由於屬性a劃分而引起的集合資訊增益為:
具體實現演算法如下:
def calcShannonEnt(dataSet): #計算樣本數 numEntries = len(dataSet) #字典 key 類別 value 該類別的樣本數 labelCounts = {} for featVec in dataSet: #the the number of unique elements and their occurance #取出最後一個元素獲得類別資訊 currentLabel = featVec[-1] #字典中不存在則新增置0,若存在則更新數量+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) #log base 2 return shannonEnt
輸入資料為樣本資料,例如:
dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'],[0, 1, 'no']]
矩陣的行數為樣本數目,每一行元素前面為特徵屬性取值,最後一個為類別資訊。
計算結果如下:
2.劃分資料集
前面實現了集合D資訊熵的計算,當基於某個屬性進行特徵劃分時,計算其資訊增益。然後選擇使資訊增益最大的屬性來進行特徵劃分。
返回樣本第axis個特徵屬性取值為value的樣本集合,同時去除掉axis該屬性值
def splitDataSet(dataSet, axis, value): #存放特徵劃分後的集合 retDataSet = [] for featVec in dataSet: #每一個樣本的第axis個特徵屬性取值為value,則返回該樣本,同時去除掉axis這一列 if featVec[axis] == value: #取0--axis 和 axis+1---length reducedFeatVec = featVec[:axis] #chop out axis used for splitting reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet
extend用於陣列元素的拼接,append用於陣列元素的增加.
輸入樣本集合,取出第0個特徵屬性值為1的樣本集合,測試如下:
處理樣本集合,獲取所有的特徵屬性,對於每一種屬性,首先獲取該屬性的所有取值可能,然後計算該屬性進行劃分引起的集合資訊增益,依次計算所有的特徵屬性,最後返回資訊增益最大的特徵屬性,來作為集合的劃分選擇。
def chooseBestFeatureToSplit(dataSet): #特徵屬性的數量,每一行資料最後一個元素為類別資訊 numFeatures = len(dataSet[0]) - 1 #the last column is used for the labels #計算樣本集合初始資訊熵 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0; bestFeature = -1 for i in range(numFeatures): #iterate over all the features #取出該特徵屬性所有取值,即矩陣第i列的所有取值set featList = [example[i] for example in dataSet]#create a list of all the examples of this feature uniqueVals = set(featList) #get a set of unique values newEntropy = 0.0 #計算該屬性取值的資訊熵 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet)/float(len(dataSet)) newEntropy += prob * calcShannonEnt(subDataSet) infoGain = baseEntropy - newEntropy #calculate the info gain; ie reduction in entropy if (infoGain > bestInfoGain): #compare this to the best gain so far bestInfoGain = infoGain #if better than current best, set to best bestFeature = i return bestFeature
其中featList是矩陣第i列的所有取值構成的list,然後進行set去重,即可得到該特徵屬性所有可能的取值。
3.遞迴構建決策樹
當對每一個樣本集合進行劃分時,
1.如果所有集合樣本同屬於同一類,則應停止劃分,返回該類別作為葉子節點類別(矩陣最後一列取值只有一種)
2.如果該樣本集合已經沒有特徵屬性,則可以返回類別中最多的類別資訊作為該葉子節點類別(矩陣只有最後一列,返回最後一列中取值最多的數值資訊)
3.有多個特徵屬性,則根據特徵屬性來進行劃分,並遞迴呼叫。
定義函式處理第二種情況,已經沒有特徵屬性,返回類別中最多的類別資訊
def majorityCnt(classList):
#key 類別 value類別次數
classCount={}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
#升序排列
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#返回次數最多的類別資訊
return sortedClassCount[0][0]
構建決策樹:
def createTree(dataSet,labels):
#取出矩陣最後一列類別資訊
classList = [example[-1] for example in dataSet]
#最後一列只有一種取值
if classList.count(classList[0]) == len(classList):
return classList[0]#stop splitting when all of the classes are equal
#矩陣只有最後一列,即沒有特徵屬性劃分,返回最後一列出現次數最多的資料作為類別資訊
if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
return majorityCnt(classList)
#選擇資訊增益最大的特徵屬性
bestFeat = chooseBestFeatureToSplit(dataSet)
#獲取該特徵屬性的標籤含義
bestFeatLabel = labels[bestFeat]
#字典存放決策樹 key 劃分特徵屬性的標籤 value具體的子節點資訊
myTree = {bestFeatLabel:{}}
#刪除該特徵屬性標籤
del(labels[bestFeat])
#該特徵屬性所有可能取值set
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels
#遞迴呼叫,進行子節點的劃分
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
測試如下:
4.matplotlib構造決策樹
返回決策樹的葉子節點數目,便於進行x座標的節點繪製以及x座標偏移。遞迴遍歷二叉樹,當某個節點不是字典型別時,葉子節點數加一。
#返回決策樹的葉子數目
def getNumLeafs(myTree):
numLeafs = 0
#取出根節點
#python3中dict.keys不支援索引,需要轉換成list
firstStr = list(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
計算決策樹的高度,便於進行y座標的繪製,高度即為所有葉子節點的高度的最大值
#返回決策樹的深度
def getTreeDepth(myTree):
maxDepth = 0
firstStr = list(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
進行決策樹的繪製,座標範圍x[0-1],y[0-1],需要根據決策樹的葉子數以及高度決定x和y方向上移動間距。起始座標位於(0.5,1)處,繪製某一個節點時,首先計算該節點的葉子數以及高度,繪製該節點以及文字資訊,然後遍歷該節點的子節點資訊,如果子節點為地點則遞迴呼叫繼續繪製,否則說明該節點為葉子節點,則直接進行繪製。每次進行繪製時,都要注意節點座標的偏移計算。
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 = list(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]
#樹高度增加 調整y大小,減少
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
#畫葉子結點 x座標
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))
#畫完該節點y座標上升至父節點高度,以便遞迴呼叫產生父節點其他子節點
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, **axprops) #no ticks
#createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
#樹的總寬度
plotTree.totalW = float(getNumLeafs(inTree))
#樹的總高度
plotTree.totalD = float(getTreeDepth(inTree))
#x,y座標的偏移
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
#起始點位於(0.5,1.0)處,即畫布的最上方中間處
plotTree(inTree, (0.5,1.0), '')
plt.show()
測試如下:
5.測試,儲存決策樹
測試決策樹,給定決策樹,給定測試樣本,即特徵屬性向量,輸出該樣本的類別資訊
def classify(inputTree,featLabels,testVec):
#決策樹的第一個劃分特徵屬性標籤
firstStr = list(inputTree.keys())[0]
#該節點的葉子結點dict
secondDict = inputTree[firstStr]
#特徵屬性標籤在特徵向量中的索引
featIndex = featLabels.index(firstStr)
#測試向量在該特徵屬性上的值
key = testVec[featIndex]
#測試向量位於哪一顆子樹
valueOfFeat = secondDict[key]
#如果該子樹為dict,則繼續遞迴查詢,否則返回該節點類別資訊
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
劃分過程類似有B樹的查詢過程,根據測試樣本向量在某特徵屬性上的取值決定測試樣本位於哪一顆子數,當該子樹為字典型別,繼續遞迴呼叫,進行屬性值的再次判斷,當子樹不是字典型別,這說明是葉子節點,節點所放資料即為該測試樣本的類別資訊,返回即可。
決策樹的儲存和載入過程,訓練好決策樹可以儲存到硬碟等,這樣可以避免每次使用都臨時計算構造決策樹。
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)
6.例項--隱形眼鏡型別判斷
給定訓練樣本資料如下所示,
特徵屬性向量標籤為['age','prescript','astigmatic','tearRate']
類別資訊為['no lenses','soft','hard']
讀取資料檔案,取出特徵向量和類別資訊
特徵向量標籤,構建決策樹
繪出決策樹:
treePlotter.createPlotter(lenseTree)
總結:
本節使用資訊增益來作為特徵屬性的劃分選擇標準,ID3演算法,能夠處理離散屬性的特徵劃分,對於連續屬性不太友好。沒有考慮過擬合的情況。
優點:計算複雜度不高,輸出結果便於理解 對於中間值缺失不敏感,可以處理不相關特徵資料
缺點:容易過擬合