《機器學習實戰》之三——決策樹
花了差不多三天時間,終於把《機器學習實戰》這本書的第三章的決策樹過了一遍,知道了決策樹中ID3的一個具體編法和流程。
【一】計算資料資訊熵
這段程式碼主要是用於計算資料的每個特徵資訊熵,資訊熵用於描述資料的混亂程度,資訊熵越大說明資料包含的資訊越多,也就是資料的波動越大。而ID3演算法採用的是資訊增益作為計算指標來評價每個特徵所包含的資訊的多少,而資訊增益方法對可取數值較多的特徵有所偏好,即本身可取數值較多的特徵本身就包含更多的資訊,為了減少這種偏好,又有C4.5和CART樹,C4.5用資訊增益率來衡量資料特徵,從而摒除了這種偏好;CART樹使用了“基尼指數”來衡量特徵,基尼指數越小說明他的資料純度就越高,選取特徵時選取劃分後使基尼指數最小的特徵作為劃分標準,即選取特徵後使資料集變得更加純,從而減少了資料集本身的混亂程度,本身決策樹做的事情就是這個。
#計算資訊熵
def calcShannonEnt(dataSet):
##step 1:計算資料集中例項的總數
numEntries = len(dataSet)
## step 2: 統計每個類別出現的次數,並存放在一個字典中
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1] #獲取每個例項中的最後一個元素,也就是當前的分類標籤:
#計算每個類別出現的次數
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel]=0
labelCounts[currentLabel] += 1
## step 3: 計算資訊熵
shannonEnt = 0.0
for key in labelCounts:
#計算每個分類結果的概率P(某個結果的概率)=某個結果出現的次數/總資料條數
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob*log( prob,2)
return shannonEnt
【二】做資料集轉換
建立資料集
#建立資料集
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers'] #特徵集合【不浮出水面是否可以生存,是否有腳蹼】
return dataSet, labels
【三】劃分資料集
選取特徵的方法是計算每個特徵的資訊增益,然後再基於資訊增益的值來選取資訊增益最大的特徵作為下一次決策樹分類的依據,這樣保證了每次選取的特徵都能最大化程度的減少資料集的資訊混亂程度。選取特徵之後需要根據該特徵下所包含的可能的取值將資料集進行切分。
##劃分資料集(待劃分的資料集、劃分資料集的特徵、特徵的值)
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
#從每條資料中去除選擇的特徵,並保留其他元素
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
抽取資料之後還牽扯到資料的新增,此段程式碼中用到了extend函式和append函式,都是做資料連線的,但是主要區別是append函式直接將連線的資料形成一個元素加入到列表中,而extend是將後段要加入的資料提取出其元素再加入到列表中去。
【四】選取資料集特徵進行資料劃分
選取特徵的方法是計算每個特徵的資訊增益值,然後找到具有資訊增益值最大的特徵作為劃分的依據進行資料集劃分,具體程式碼為:
##選取最好的的資料集劃分方式,也就是資訊增益最大的特徵
def chooseBestFeatureToSplit(dataSet):
#選取資料集中第一條資料的長度並減1(eg:[1,1,“yes”],最後一個元素為分類結果,需要排除結果,只取特徵)
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)#資料集的資訊熵
bestInfoGain = 0.0
bestFeature = -1 #最好的特徵,初始化為-1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]#從資料集中將每條資料的第i個特徵去除
uniqueVals = set(featList)#去重,得到第i個特徵的所有取值
newEntropy = 0.0
#遍歷第i個特徵的每一個特徵值,計算特徵i的條件熵
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)#根據第i個特徵,並且i特徵值為value,劃分資料集
prob = len(subDataSet)/float(len(dataSet))#計算P(特徵i取值為value)的概率
newEntropy += prob*calcShannonEnt(subDataSet) #計算條件熵
infoGain = baseEntropy-newEntropy #計算資訊增益
#記錄資訊增益最大的特徵
if(infoGain>bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
由於大部分程式碼都添加了註釋,文中就不再細述
【五】尋找具有最大類別數量的葉節點
- 決策樹構建結束的標準是該分支下面所有的資料都具有相同的分類,如果所有資料都具有相同的分類,則得到的葉子結點或者終止塊,任何到達這個葉子結點的必屬於這個分類。
- 但是分類過程中不可能到最後分支之後所有的資料集都屬於同一類,因為存在噪聲資料,所以我們是找到分類結束時某個類別最多的類別作為該分支的葉節點的取值,下述程式碼就是找到數量最多的那一類作為葉節點的取值
##尋找具有最大類別數量的葉節點
#針對所有特徵都用完,但是最後一個特徵中類別還是存在很大差異,無法進行劃分,
#此時選取該類別中最多的類,作為劃分的返回值,majoritycnt的作用就是找到類別最多的一個作為返回值
def majorityCnt(classList):
classCount={} #建立字典
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0 #如果現階段的字典中缺少這一類的特徵,建立到字典中並令其值為0
classCount[vote]+=1 #迴圈一次,在對應的字典索引vote的數量上加一
#利用sorted方法對class count進行排序,
#並且以key=operator.itemgetter(1)作為排序依據降序排序因為用了(reverse=True)
#3.0以上的版本不再有iteritems而是items
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
其中用到了operator中的items函式,python3.x以上的版本不再有iteritems而是items,而《機器學習實戰》程式碼時基於2.x的版本的程式碼,所以經常會報錯,建議大家寫程式碼時邊寫邊思考,items() 方法是把dict物件轉換成了包含tuple的list,栗子:
【六】遞迴構建決策樹
- 構建決策樹用到的最基本的思想是遞迴,而遞迴結束的條件有兩個:(1)在該分支下所有的類標籤都相同,即在該支葉節點下所有的樣本都屬於同一類;(2)使用了所有的特徵,但是樣本資料還是存在一定的分歧,這個時候就要使用上訴的majorityCnt(classList)函數了。
- 在樹的構建過程中,是每一次利用chooseBestFeatureToSplit(dataSet)函式找到在剩下的資料集中資訊增益最大的特徵作為分類的依據,然後每次生成的tree是 myTree={bestFeatLabel:{}}這樣的形式,保證了每次遞迴都在字典中存在子字典,從而最後完成決策樹的構建。同時在構建過程中使用過的標籤需要進行刪除,從而不影響下一次特徵的選取,同樣也用到了set()函式。
##建立樹
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet] #提取dataset中的最後一欄——種類標籤
if classList.count(classList[0])==len(classList):#計算classlist[0]出現的次數,如果相等,說明都是屬於一類,不用繼續往下劃分
return classList[0]
if len(dataSet[0])==1:#看還剩下多少個屬性,如果只有一個屬性,但是類別標籤又多個,就直接用majoritycnt方法進行整理 選取類別最多的作為返回值
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)#選取資訊增益最大的特徵作為下一次分類的依據
bestFeatLabel = labels[bestFeat] #選取特徵對應的標籤
myTree = {bestFeatLabel:{}}#建立tree字典,緊跟現階段最優特徵,下一個特徵位於第二個大括號內,迴圈遞迴
del(labels[bestFeat]) #使用過的特徵從中刪除
featValues = [example[bestFeat] for example in dataSet]#特徵值對應的該欄資料
uniqueVals = set(featValues)#找到featvalues所包含的所有元素,同名元素算一個
for value in uniqueVals:
subLabels = labels[:]#子標籤的意思是迴圈一次之後會從中刪除用過的標籤 ,剩下的就是子標籤了
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)#迴圈遞迴生成樹
return myTree
【七】使用matplotlib對生成的樹進行註釋
此過程中主要用到的是matplotlib中的annotate包,具體有很多方法和函式,我也只是過了一下書上的東西,有興趣繼續深入的可以參考:https://www.jianshu.com/p/1411c51194de
http://blog.csdn.net/u013457382/article/details/50956459
遇到的問題是中文邊框轉碼之後不顯示,英文邊框之後自適應顯示
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 createPlot():
fig = plt.figure(1, facecolor='white')
fig.clf()
createPlot.ax1 = plt.subplot(111, frameon=False)
plotNode(U'決策節點', (0.5, 0.1),(0.1,0.5), decisionNode)
plotNode(U'葉節點', (0.8,0.1),(0.3,0.8),leafNode)
plt.show()
【八】計算樹的葉節點數目和樹的層數
getNumLeafs()函式和getTreeDepth()函式在結構和方法上都比較類似,因為tree字典中的每一個大括號的第一個字元都是它對應的鍵值,所以我們只需要判斷第二個還是不是鍵值,如果是的話說明還有樹或者深度,都是用了遞迴的思想,在每次迴圈中都先找到該字典中的第一個字元,然後判斷在該鍵值下的第二個字元是不是還是鍵,是的話說明還有深度或者層數,不是的話說明已經找完了。其中遇到了這樣的問題:
注意:python2.x的版本中是firstStr=myTree.keys()[],但是dict_keys型的資料不支援索引,所以強制轉換成list即可,即 firstStr=list(myTree.keys())[0]。
##獲取葉節點的數目
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs +=1
return numLeafs
##獲取樹的層數
def getTreeDepth(myTree):
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
thisDepth =1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth>maxDepth:
maxDepth = thisDepth
return maxDepth
【九】生成決策樹圖形
xOff和yOff用來記錄當前要畫的葉子結點的位置。
- xOff:
-(1) 畫布的範圍x軸和y軸都是0到1,我們希望所有的葉子結點平均分佈在x軸上。totalW記錄葉子結點的個數,那麼 1/totalW正好是每個葉子結點的寬度
-(2) 如果葉子結點的座標是 1/totalW , 2/totalW, 3/totalW, …, 1的話,就正好在寬度的最右邊,為了讓座標在寬度的中間,需要減去0.5 / totalW 。所以createPlot函式中,初始化plotTree.xOff 的值為-0.5/plotTree.totalW。這樣每次 xOff + 1/totalW,正好是下1個結點的準確位置 - yOff:yOff的初始值為1,每向下遞迴一次,這個值減去 1 / totalD
- cntrPt:cntrPt用來記錄當前要畫的樹的樹根的結點位置
#作用是計算tree的中間位置 cntrpt起始位置,parentpt終止位置,txtstring:文字標籤資訊
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]#找到x和y的中間位置
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[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#每繪製一次圖,將y的座標減少1.0/plottree.totalD,間接保證y座標上深度的
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為全域性變數,繪製圖像的控制代碼,subplot為定義了一個繪圖,111表示figure中的圖有1行1列,即1個,最後的1代表第一個圖
# frameon表示是否繪製座標軸矩形
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()
【十】生成樹之後利用測試資料測試程式碼
構建生成樹之後需要利用測試資料測試程式碼的分類效果,具體程式碼如下:
#用於多條資料的測試,並且最後要計算出分類的準確率
def testEfficiency(inputTree, featLabels, testData):
flag = 0
for i in range(len(testData)):
result = classify(inputTree,featLabels,testData[i][:6])
if(result==testData[i][-1]):
flag +=1
print("the calculte result is %s, he true result is %s" % (result,testData[i][-1]))
print('the data number is %d,but the right number is %d'%(len(testData),flag))
程式碼也是使用了遞迴的思想,即每次都找到測試資料在每個特徵下的值,根據值選取合適的分支,到了下一個分支再進行一次迴圈直到到了決策樹的葉節點為止,從而將測試資料歸類到對應的分類之下。
【十一】編寫對於多資料的測試程式碼
這段程式碼是書上沒有,主要是用於有多個數據集時的測試,相對簡單,就是每次提取出測試資料中的一組測試資料用於程式碼的測試,並且最後計算出分類正確的個數和正確率。這裡我是參考https://blog.csdn.net/cxjoker/article/details/79501887中的內容學習到的
##用於多條資料的測試,並且最後要計算出分類的準確率
def testEfficiency(inputTree, featLabels, testData):
flag = 0
for i in range(len(testData)):
result = classify(inputTree,featLabels,testData[i][:6])
if(result==testData[i][-1]):
flag +=1
print("the calculte result is %s, he true result is %s" % (result,testData[i][-1]))
print('the data number is %d,but the right number is %d'%(len(testData),flag))
【十二】儲存與讀取決策樹
儲存與讀取決策樹主要是用到了pickle模組,具體我也沒有細細的去深究pickle模組,具體可以參考http://www.php.cn/python-tutorials-372984.html。
因為早期的pickle程式碼是二進位制的pickle(除了最早的版本外)是二進位制格式的,所以你應該帶 ‘rb’ 標誌開啟檔案。
##使用pickle模組儲存決策樹
def storeTree(inputTree,filename):
import pickle
fw = open(filename, 'wb')
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename,'rb')
return pickle.load(fr)
【十三】利用決策樹預測隱型眼鏡型別
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate']
lensesTree = trees.createTree(lenses,lensesLabels)
treePlotter.createPlot(lensesTree)
結果如圖所示