文章原創(chuàng),最近更新:2018-08-20
本章節(jié)的主要內(nèi)容是:
重點介紹項目案例1:判定魚類和非魚類測試算法:測試和存儲分類器的代碼。
1.決策樹項目案例介紹:
項目案例1:
判定魚類和非魚類
項目概述:
- 根據(jù)以下 2 個特征,將動物分成兩類:魚類和非魚類。
- 特征: 1. 不浮出水面是否可以生存 2. 是否有腳蹼
開發(fā)流程:
- 收集數(shù)據(jù):可以使用任何方法
- 準(zhǔn)備數(shù)據(jù):樹構(gòu)造算法只適用于標(biāo)稱型數(shù)據(jù),因此數(shù)值型數(shù)據(jù)必須離散化
- 分析數(shù)據(jù):可以使用任何方法,構(gòu)造樹完成之后,我們應(yīng)該檢查圖形是否符合預(yù)期
- 訓(xùn)練算法:構(gòu)造樹的數(shù)據(jù)結(jié)構(gòu)
- 測試算法:使用決策樹執(zhí)行分類
- 使用算法:此步驟可以適用于任何監(jiān)督學(xué)習(xí)算法,而使用決策樹可以更好地理解數(shù)據(jù)的內(nèi)在含義
數(shù)據(jù)集介紹

2.代碼匯總
2.1測試數(shù)據(jù)集
首先創(chuàng)建一個名為trees.py的文件,createDataSet()函數(shù)錄入到trees.py文件.
from math import log
import operator
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
2.2計算給定數(shù)據(jù)集的香農(nóng)熵的函數(shù)
這段代碼主要是計算給定數(shù)據(jù)集的熵,創(chuàng)建一個函數(shù)calcShannonEn()函數(shù)錄入到trees.py文件.
def calcShannonEnt(dataSet):
# 獲取數(shù)據(jù)集dataSet列表的長度,表示計算參與訓(xùn)練的數(shù)據(jù)量
numEntries=len(dataSet)
# 新建一個空字典labelCounts,用以統(tǒng)計每個標(biāo)簽出現(xiàn)的次數(shù),進而計算概率
labelCounts={}
for featVec in dataSet:
# featVec[-1]獲取了daatSet中每行最后一個數(shù)據(jù),作為字典中的key(標(biāo)簽)
currentLabel = featVec[-1]
# 以currentLabel作為key加入到字典labelCounts.
# 如果當(dāng)前的鍵值不存在,則擴展字典并將當(dāng)前鍵值加入字典。每個鍵值都記錄了當(dāng)前類別出現(xiàn)的次數(shù)。
# 鍵值存在則則對應(yīng)value+1,否則為0
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel]=0
labelCounts[currentLabel] += 1
# 對于 label 標(biāo)簽的占比,求出 label 標(biāo)簽的香農(nóng)熵
shannonEnt = 0.0
for key in labelCounts:
# 計算分類概率prob=標(biāo)簽發(fā)生頻率,labelCounts[key]除以數(shù)據(jù)集長度numEntries
prob = float(labelCounts[key])/numEntries
# 計算香農(nóng)熵,以2為底求對數(shù)
shannonEnt -=prob * log(prob,2)
return shannonEnt
測試代碼及其結(jié)果如下:
import trees
a, b = trees.createDataSet()
trees.calcShannonEnt(a)
Out[90]: 0.9709505944546686
2.3劃分?jǐn)?shù)據(jù)集的函數(shù)代碼
這個函數(shù)的是作用是當(dāng)我們按某個特征劃分?jǐn)?shù)據(jù)集時,把劃分后剩下的元素抽取出來,形成一個新的子集,用于計算條件熵。
創(chuàng)建一個函數(shù)splitDataSet()函數(shù)錄入到trees.py文件.
具體相關(guān)知識點,可參見:3-2節(jié) 決策樹|劃分?jǐn)?shù)據(jù)集|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
def splitDataSet(dataSet,axis,value):
"""
splitDataSet(通過遍歷dataSet數(shù)據(jù)集,求出index對應(yīng)的column列的值為value的行)
就是依據(jù)index列進行分類,如果index列的數(shù)據(jù)等于value的時候,就要index劃分到我們創(chuàng)建的新的數(shù)據(jù)集中
Args:
dataSet:數(shù)據(jù)集 待劃分的數(shù)據(jù)集
axis:表示每一行的index列 特征的坐標(biāo),等于0,第0個特征為0或者1
value:表示index列對應(yīng)的value值 需要返回的特征的值
Returns:
index列為value的數(shù)據(jù)集[該數(shù)據(jù)集需要排除axis列]
"""
retDataSet = []
# index列為value的數(shù)據(jù)集[該數(shù)據(jù)集需要排除index列]
# 判斷index列的值是否等于value
# 遍歷數(shù)據(jù)集,將axis上的數(shù)據(jù)和value值進行對比
for featVec in dataSet:
# 如果待檢測的特征axis和指定的特征value相等
if featVec[axis] == value:
# 從第0開始,一旦發(fā)現(xiàn)第axis符合要求,就將數(shù)據(jù)0-axis保存至reduceFeatVec
reducedFeatVec =featVec[:axis]
# 將指定的數(shù)據(jù)的axis+1位到末尾添加至reducedFeatVec,保持?jǐn)?shù)據(jù)完整性
reducedFeatVec.extend(featVec[axis+1:])
# 收集結(jié)果值除掉index列的reducedFeatVec收據(jù)集添加到retDataSet數(shù)據(jù)集
retDataSet.append(reducedFeatVec)
return retDataSet
測試代碼及其結(jié)果如下:
import trees
mydata,labels=trees.createDataSet()
mydata
Out[111]: [[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
trees.splitDataSet(mydata,0,1)
Out[112]: [[1, 'maybe'], [1, 'yes'], [0, 'no']]
2.4選擇最好的數(shù)據(jù)集劃分方式的函數(shù)代碼
接下來我們將遍歷整個數(shù)據(jù)集,循環(huán)計算香農(nóng)熵和 splitDataSet()函數(shù),找到最好的特征劃分方式。熵計算將會告訴我們?nèi)绾蝿澐謹(jǐn)?shù)據(jù)集是最好的數(shù)據(jù)組織方式.
創(chuàng)建一個函數(shù)chooseBestFeatTopSplit()函數(shù)錄入到trees.py文件.
具體相關(guān)知識點,可參見:3-3節(jié) 決策樹|選擇最好的數(shù)據(jù)集劃分方式|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
def chooseBestFeatTopSplit(dataSet):
"""chooseBestFeatureToSplit(選擇最好的特征)
Args:
dataSet 數(shù)據(jù)集
Returns:
bestFeature 最優(yōu)的特征列
"""
# 求第一行有多少列的 Feature, 減去1,是因為最后一列是label列
numFeatures = len(dataSet[0])-1
# 計算沒有經(jīng)過劃分的數(shù)據(jù)的香農(nóng)熵
baseEntropy = calcShannonEnt(dataSet)
# 最優(yōu)的信息增益值
bestInfoGain = 0.0
#最優(yōu)的Featurn編號
bestFeature = -1
for i in range(numFeatures):
# 創(chuàng)建唯一的分類標(biāo)簽列表,獲取第i個的所有特征(信息元縱排列?。? featList = [example[i] for example in dataSet]
"""
print(featList)結(jié)果為
[1, 1, 1, 0, 0]
[1, 1, 0, 1, 1]
"""
# 使用set集,排除featList中的重復(fù)標(biāo)簽,得到唯一分類的集合
uniqueVals = set(featList)
"""
print(uniqueVals)結(jié)果為
{0, 1}
{0, 1}
"""
newEntropy = 0.0
# 遍歷當(dāng)次uniqueVals中所有的標(biāo)簽value(這里是0,1)
for value in uniqueVals:
# 對第i個數(shù)據(jù)劃分?jǐn)?shù)據(jù)集, 返回所有包含i的數(shù)據(jù)(已排除第i個特征)
subDataSet = splitDataSet(dataSet, i, value)
"""
print(subDataSet)結(jié)果為
[[1, 'no'], [1, 'no']]
[[1, 'yes'], [1, 'yes'], [0, 'no']]
[[1, 'no']]
[[1, 'yes'], [1, 'yes'], [0, 'no'], [0, 'no']]
"""
# 計算包含個i的數(shù)據(jù)占總數(shù)據(jù)的百分比
prob = len(subDataSet)/float(len(dataSet))
"""
print(prob)結(jié)果為
0.4
0.6
0.2
0.8
"""
# 計算新的香農(nóng)熵,不斷進行迭代,這個計算過程僅在包含指定特征標(biāo)簽子集中進行
newEntropy += prob * calcShannonEnt(subDataSet)
"""
print(calcShannonEnt(subDataSet))
0.0
0.9182958340544896
0.0
1.0
print(newEntropy)結(jié)果為
0.0
0.5509775004326937
0.0
0.8
"""
# 計算信息增益
infoGain = baseEntropy - newEntropy
# 如果信息增益大于最優(yōu)增益,即新增益newEntropy越小,信息增益越大,分類也就更優(yōu)(分類越簡單越好)
"""
print(infoGain)結(jié)果為
0.4199730940219749
0.17095059445466854
"""
if (infoGain > bestInfoGain):
# 更新信息增益
bestInfoGain = infoGain
# 確定最優(yōu)增益的特征索引
bestFeature = i
# 更新信息增益
# 返回最優(yōu)增益的索引
return bestFeature
測試代碼及其 結(jié)果如下:
import trees
myDat,labels=trees.createDataSet()
myDat
Out[182]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
trees.chooseBestFeatTopSplit(myDat)
Out[183]: 0
2.5 遞歸構(gòu)建決策樹
創(chuàng)建分別函數(shù)majorityCnt()以及createTree()錄入到trees.py文件.
具體相關(guān)知識點,可參見:3-4節(jié) 決策樹|遞歸構(gòu)建決策樹|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
2.5.1篩選出現(xiàn)次數(shù)最多的分類標(biāo)簽名稱
如果數(shù)據(jù)集已經(jīng)處理了所有的屬性,但是類標(biāo)簽依然不是唯一的,此時我們需要決定如何定義該葉子節(jié)點,在這種情況下,我們通常會采用多數(shù)表決的方法決定該葉子節(jié)點的分類.
#篩選出現(xiàn)次數(shù)最多的分類標(biāo)簽名稱
def majorityCnt(classList):
"""
majorityCnt(篩選出現(xiàn)次數(shù)最多的分類標(biāo)簽名稱)
Args:
classList 類別標(biāo)簽的列表
Returns:
sortedClassCount[0][0] 出現(xiàn)次數(shù)最多的分類標(biāo)簽名稱
假設(shè)classList=['yes', 'yes', 'no', 'no', 'no']
"""
classCount={}
for vote in classList:
if vote not in classCount.keys():classCount[vote]= 0
classCount[vote] += 1
"""
print(classCount[vote])的結(jié)果為:
{'yes': 1}
{'yes': 2}
{'yes': 2, 'no': 1}
{'yes': 2, 'no': 2}
{'yes': 2, 'no': 3}
"""
sortedClassCount =sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
"""
print(sortedClassCount)的結(jié)果為:
[('no', 3), ('yes', 2)]
"""
return sortedClassCount[0][0]
測試代碼及其結(jié)果如下:
import trees
classList=['yes', 'yes', 'no', 'no', 'no']
majorityCnt(classList)
Out[45]: 'no'
2.5.2遞歸構(gòu)建決策樹
決策樹是一個遞歸算法,偽代碼如下:
def createBranch():
檢測數(shù)據(jù)集中的所有數(shù)據(jù)的分類標(biāo)簽是否相同:
If so return 類標(biāo)簽
Else:
尋找劃分?jǐn)?shù)據(jù)集的最好特征(劃分之后信息熵最小,也就是信息增益最大的特征)
劃分?jǐn)?shù)據(jù)集
創(chuàng)建分支節(jié)點
for 每個劃分的子集
調(diào)用函數(shù) createBranch (創(chuàng)建分支的函數(shù))并增加返回結(jié)果到分支節(jié)點中
return 分支節(jié)點

決策樹一般使用遞歸的方法生成。
編寫遞歸函數(shù)有一個好習(xí)慣,就是先考慮結(jié)束條件。生成決策樹結(jié)束的條件有兩個:其一是劃分的數(shù)據(jù)都屬于一個類,其二是所有的特征都已經(jīng)使用了。在第二種結(jié)束情況中,劃分的數(shù)據(jù)有可能不全屬于一個類,這個時候需要根據(jù)多數(shù)表決準(zhǔn)則確定這個子數(shù)據(jù)集的分類。
在非結(jié)束的條件下,首先選擇出信息增益最大的特征,然后根據(jù)其分類。分類開始時,記錄分類的特征到?jīng)Q策樹中,然后在特征標(biāo)簽集中刪除該特征,表示已經(jīng)使用過該特征。根據(jù)選中的特征將數(shù)據(jù)集分為若干個子數(shù)據(jù)集,然后將子數(shù)據(jù)集作為參數(shù)遞歸創(chuàng)建決策樹,最終生成一棵完整的決策樹
# 創(chuàng)建樹的函數(shù)代碼
def createTree(dataSet, labels):
"""
createTree(創(chuàng)建樹)
Args:
dataSet 數(shù)據(jù)集
labels 標(biāo)簽列表:標(biāo)簽列表包含了數(shù)據(jù)集中所有特征的標(biāo)簽。最后代碼遍歷當(dāng)前選擇
Returns:
myTree 標(biāo)簽樹:特征包含的所有屬性值,在每個數(shù)據(jù)集劃分上遞歸待用函數(shù)createTree(),
得到的返回值將被插入到字典變量myTree中,因此函數(shù)終止執(zhí)行時,字典中將會嵌套很多代
表葉子節(jié)點信息的字典數(shù)據(jù)。
"""
#取得dataSet的最后一列數(shù)據(jù)保存在列表classList中
classList = [example[-1] for example in dataSet]
#如果classList中的第一個值在classList中的總數(shù)等于長度,也就是說classList中所有的值都一樣
#也就等價于當(dāng)所有的類別只有一個時停止
if classList.count(classList[0])==len(classList):
return classList[0]
#當(dāng)數(shù)據(jù)集中沒有特征可分時也停止
if len(dataSet[0])==1:
#通過majorityCnt()函數(shù)返回列表中最多的分類
return majorityCnt(classList)
#通過chooseBestFeatTopSplit()函數(shù)選出劃分?jǐn)?shù)據(jù)集最佳的特癥
bestFeat = chooseBestFeatTopSplit(dataSet)
#最佳特征名 = 特征名列表中下標(biāo)為bestFeat的元素
bestFeatLabel=labels[bestFeat]
# 構(gòu)造樹的根節(jié)點,多級字典的形式展現(xiàn)樹,類似多層json結(jié)構(gòu)
myTree={bestFeatLabel:{}}
# 刪除del列表labels中的最佳特征(就在labels變量上操作)
del(labels[bestFeat])
#取出所有訓(xùn)練樣本最佳特征的值形成一個list
featValues = [example[bestFeat] for example in dataSet]
# 通過set函數(shù)將featValues列表變成集合,去掉重復(fù)的值
uniqueVals = set(featValues)
for value in uniqueVals:
#復(fù)制類標(biāo)簽并將其存儲在新列表subLabels中
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
測試代碼及其結(jié)果如下:
import trees
myDat,labels=createDataSet()
myTree =createTree(myDat,labels)
myTree
Out[55]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
2.6使用文本注解繪制樹節(jié)點的函數(shù)代碼
將以下代碼錄入到treePlotter.py文件.
具體相關(guān)知識點,可參見:3-5節(jié) 決策樹|使用文本注解繪制樹節(jié)點|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
《機器學(xué)習(xí)實戰(zhàn)》書中,該部分的代碼有些混亂。重新構(gòu)造了代碼,創(chuàng)建一個類。其中,繪制最基本的樹節(jié)點是如下代碼:
#導(dǎo)入matplotlib的pyplot繪圖模塊并命名為plt
import matplotlib.pyplot as plt
# boxstyle是文本框類型,fc是邊框粗細(xì),sawtooth是鋸齒形
decisionNode = dict(boxstyle="sawtooth",fc="0.8")
leafNode = dict(boxstyle="round4",fc="0.8")
# arrowprops: 通過arrowstyle表明箭頭的風(fēng)格或種類。
arrow_args=dict(arrowstyle="<-")
# annotate 注釋的意思
#plotNode()函數(shù)繪制帶箭頭的注解,sub_ax:使用figure命令來產(chǎn)生子圖, node_text:節(jié)點的文字標(biāo)注,start_pt:箭頭起點位置(上一節(jié)點位置),end_pt:箭頭結(jié)束位置, node_type:節(jié)點屬性
def plot_node(sub_ax, node_text, start_pt, end_pt, node_type):
sub_ax.annotate(node_text,
xy = end_pt, xycoords='axes fraction',
xytext = start_pt, textcoords='axes fraction',
va='center', ha='center', bbox=node_type, arrowprops=arrow_args)
if __name__ == '__main__':
fig = plt.figure(1, facecolor='white')
#清空繪圖區(qū)
fig.clf()
axprops = dict(xticks=[], yticks=[]) #去掉坐標(biāo)軸
sub_ax = plt.subplot(111, frameon=False, **axprops)
#繪制節(jié)點
plot_node(sub_ax, 'a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
plot_node(sub_ax, 'a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
輸出結(jié)果如下:

2.7測試算法:使用決策樹執(zhí)行分類代碼
依靠訓(xùn)練數(shù)據(jù)構(gòu)造了決策樹之后,我們可以將它用于實際數(shù)據(jù)的分類。在執(zhí)行數(shù)據(jù)分類時,需要決策樹以及用于決策樹的標(biāo)簽向量。然后,程序比較測試數(shù)據(jù)與決策樹上的數(shù)值,遞歸執(zhí)行該過程直到進入葉子結(jié)點;最后將測試數(shù)據(jù)定義為葉子結(jié)點所屬的類型。
創(chuàng)建一個函數(shù)classify()錄入到trees.py文件.
具體相關(guān)知識點,可參見:3-6節(jié) 決策樹|測試和存儲分類器|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
def classify(inputTree, featLabels, testVec):
# 因為并不知道按特征分類的先后順序,所以要寫一個分類器
"""classify(給輸入的節(jié)點,進行分類)
Args:
inputTree 是輸入的決策樹對象
featLabels Feature是我們要預(yù)測的特征值的label,如:['throat','mustache']
testVec 是要預(yù)測的特征值向量,如[0,0]
Returns:
classLabel 分類的結(jié)果值,需要映射label才能知道名稱
"""
# 存儲決策樹第一個節(jié)點
firstStr=list(inputTree.keys())[0]
"""
myTree={'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
labels=['no surfacing', 'flippers']
print(firstStr)的結(jié)果為:
'no surfacing'
"""
# 將第一個節(jié)點的值存到secondDict字典中
secondDict = inputTree[firstStr]
"""
print(secondDict)的結(jié)果為:
{0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}
"""
# 判斷根節(jié)點名稱獲取根節(jié)點在label中的先后順序,這樣就知道輸入的testVec怎么開始對照樹來做分類
featIndex = featLabels.index(firstStr)
"""
print(featIndex)的結(jié)果為:
0
"""
for key in secondDict.keys():
"""
print(secondDict.keys())的結(jié)果為:
dict_keys([0, 1])
"""
if testVec[featIndex]==key:
# 判斷分枝是否結(jié)束:判斷secondDict[key]是否是dict類型,如果是就遞歸,不是就輸出當(dāng)前鍵值為結(jié)果
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
測試代碼以及結(jié)果如下:
import trees
myDat, labels = trees.createDataSet()
myTree = trees.createTree(myDat, labels[:])
Out[35]: trees.classify(myTree, labels, [1, 0])
'no'
Out[36]: trees.classify(myTree, labels, [1, 1])
'yes'
2.8使用算法:決策樹的存儲
可以使用Python模塊pickle序列化對象,參見下面的程序。序列化對象可以在磁盤上保存對象,并在需要的時候讀取出來。
創(chuàng)建分別函數(shù)storeTree()/grabTree()錄入到trees.py文件.
具體相關(guān)知識點,可參見:3-6節(jié) 決策樹|測試和存儲分類器|機器學(xué)習(xí)實戰(zhàn)-學(xué)習(xí)筆記
def storeTree(inputTree,filename):
import pickle
# wb二進制寫模式
fw = open(filename,"wb")
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
# rb二進制文件讀取
fr=open(filename,"rb")
return pickle.load(fr)
測試代碼以及結(jié)果如下:
import trees
myDat, labels = trees.createDataSet()
myTree = trees.createTree(myDat, labels[:])
storeTree(myTree,'classifierStorage.txt')
grabTree('classifierStorage.txt')
Out[51]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}