python机器学习笔记:ID3决策树算法实战
前面學習了決策樹的算法原理,這里繼續對代碼進行深入學習,并學習ID3的算法實踐過程,如果覺得這篇文章太乏味的話,可以直接看前一篇即可。
ID3算法是一種貪心算法,用來構造決策樹,ID3算法起源于概念學習系統(CLS),以信息熵的下降速度為選取測試屬性的標準,即在每一個節點選取還尚未被用來劃分的具有最高信息增益的屬性作為劃分標準,然后繼續這個過程,直到生成的決策樹能完美的分類訓練樣例。
ID3算法的背景知識
ID3算法最早是由羅斯昆蘭(J. Ross Quinlan)于1975年在悉尼大學提出的一種分類預測算法,算法的核心是“信息熵”。ID3算法通過計算每個屬性的信息增益,認為信息增益高的是好屬性,每次劃分選取信息增益最高的屬性為劃分標準,重復這個過程,直至生成一個能完美分類訓練樣例的決策樹。
決策樹是對數據進行分類,以此達到預測的目的。該決策樹方法先根據訓練集數據形成決策樹,如果該樹不能對所有對象給出正確的分類,那么選擇一些例外加入到訓練集數據中,重復該過程一直到形成正確的決策集。決策樹代表著決策集的樹形結構。
決策樹由決策結點、分支和葉子組成。決策樹中最上面的結點為根結點,每個分支是一個新的決策結點,或者是樹的葉子。每個決策結點代表一個問題或決策,通常對應于待分類對象的屬性。每一個葉子結點代表一種可能的分類結果。沿決策樹從上到下遍歷的過程中,在每個結點都會遇到一個測試,對每個結點上問題的不同的測試輸出導致不同的分支,最后會到達一個葉子結點,這個過程就是利用決策樹進行分類的過程,利用若干個變量來判斷所屬的類別。
ID3算法數據描述
所使用的樣本數據有一定的要求,ID3是:
描述-屬性-值相同的屬性必須描述每個例子和有固定數量的價值觀。
預定義類-實例的屬性必須已經定義的,也就是說,他們不是學習的ID3。
離散類-類必須是尖銳的鮮明。連續類分解成模糊范疇(如金屬被“努力,很困難的,靈活的,溫柔的,很軟”都是不可信的。
足夠的例子——因為歸納概括用于(即不可查明)必須選擇足夠多的測試用例來區分有效模式并消除特殊巧合因素的影響。
ID3決定哪些屬性如何是最好的。一個統計特性,被稱為信息增益,使用熵得到給定屬性衡量培訓例子帶入目標類分開。信息增益最高的信息(信息是最有益的分類)被選擇。為了明確增益,我們首先從信息論借用一個定義,叫做熵。每個屬性都有一個熵。
ID3決策樹概述
ID3決策樹是一種非常重要的用來處理分類問題的結構,它形似一個嵌套N層的IF…ELSE結構,但是它的判斷標準不再是一個關系表達式,而是對應的模塊的信息增益。它通過信息增益的大小,從根節點開始,選擇一個分支,如同進入一個IF結構的statement,通過屬性值的取值不同進入新的IF結構的statement,直到到達葉子節點,找到它所屬的“分類”標簽。
它的流程圖是一顆無法保證平衡的多叉樹,每一個父節點都是一個判斷模塊,通過判斷,當前的向量會進入它的某一個子節點中,這個子節點是判斷模塊或者終止模塊(葉子節點),當且僅當這個向量到達葉子節點,它也就找到了它的“分類”標簽。
ID3決策樹通過一個固定的訓練集是可以形成一顆永久的“樹”的,這課樹可以進行保存并且運用到不同的測試集中,唯一的要求就是測試集和訓練集需要是結構等價的。這個訓練過程就是根據訓練集創建規則的過程,這也是機器學習的過程。
ID3決策樹的一個巨大缺陷是:它將產生過度匹配問題。這里在不討論信息增益的前提下,有這樣一個例子:人的屬性中有性別和年齡兩個屬性,由于人的性別只有男和女兩種,年齡有很多種分支,當它有超過兩個分支的時候,在用信息增益選擇新的屬性的時候,會選擇年齡而不是性別,因為ID3決策樹在使用信息增益來劃分數據集的時候會傾向于選擇屬性分支更多的一個;另外一個缺陷是,人的年齡假定為1~100,如果不進行離散化,即區間的劃分,那么在選擇年齡這個屬性的時候,這棵決策樹會產生最多100個分支,這是非常可怕而且浪費空間和效率的,考慮這 樣一種情況:兩個人的其他所有屬性完全相同,他們的分類都是"A",然而在年齡這一個樹節點中分支了,而這個年齡下有一個跟這兩個人很像,卻不屬于“A”類別的人,由于ID3決策樹無法處理連續性數據,那么這兩個人很有可能被劃分到兩個分類中,這是不合理的,這也是C4.5決策樹考慮的問題。
ID3決策樹算法推導
如果以前沒有接觸過決策樹,完全不用擔心,它的概念非常簡單,即使不知道它也可以通過簡單的圖像了解其工作原理,下圖就是一個決策樹,正方形代表判斷模型(decision block),橢圓形代表終止模塊(terminating block),表示已經得出結論,可以終止運行。從判斷模塊引出的左右箭頭稱作分支(branch),它首先檢測發送郵件域名地址。如果地址為myEmployer.com,則將其分類“無聊時需要閱讀的郵件”中。如果郵件不是來自這個域名,則檢查郵件內容里是否包含單詞曲棍球,如果包含則將郵件歸類到“需要及時處理的盆友郵件”,如果不包含則將郵件歸類到“無需閱讀的垃圾郵件”。
決策樹很多任務都是為了數據中蘊含的知識信息,因此決策樹可以使用不熟悉的數據集合,并從中提取出一系列的規則,機器學習算法最終將使用這些機器從數據及中創造的規則。專家系統中經常使用決策樹,而且決策樹給出結果往往可以匹敵在當前領域具有幾十年工作經驗的人類專家。
現在我們已經大致了解了決策樹可以完成哪些任務,接下來我們將學習如何從一堆原始數據中構造決策樹,首先我們討論構造決策樹的方法,以及如何編寫決策樹的Python代碼;接著剔除一些度量算法成功率的方法;最后使用遞歸建立分類器,并且使用Matplotlib繪制決策樹圖,構造完成決策樹分類器之后,我們將輸入一些隱形眼鏡的處方數據,并由決策樹分類器預測需要的鏡片類型。
1,決策樹的構造
在一步步地構造決策樹算法的時候,首先我們討論數學上如何使用信息論劃分數據集,然后編寫代碼將理論應用到具體的數據集上,最后編寫代碼構建決策樹。
在構造決策樹時,我們需要解決的第一個問題就是,當前數據集上哪個特征在劃分數據分類時起決定性作用。為了找到決定性的特征,劃分出最好的結果,我們必須評估每個特征。完成測試之后,原始數據集就被劃分為幾個數據子集,這些數據子集會分布在第一個決策點的所有分支上,如果某個分支下的數據屬于同一類型,則當前無需閱讀的垃圾郵件已經正確的劃分數據分類,無需進一步對數據集進行分割,如果數據子集內的數據不屬于同一類型,則需要重復劃分數據子集的過程,如何劃分數據子集的算法和劃分原始數據集的方法相同,知道所有具有相同類型的數據均在一個數據子集內。
創建分支的偽代碼函數createBranch() 如下所示:
檢測數據集中的每個子項是否屬于同一分類:
If so retrun 類標簽;
Else
尋找劃分數據集的最好特征
劃分數據集
創建分支節點
for 每個劃分的子集
調用函數createBranch并增加返回結果到分支節點中
return 分支節點
上面的偽代碼createBranch是一個遞歸函數,在倒數第二行直接調用了它自己,后面我們將把上面的偽代碼轉換為Python代碼,這里我們需要了解一下算法是如何劃分數據集的。
1.1 決策樹的一般流程
(1) 收集數據:可以使用任何方法
(2) 準備數據:樹構造算法只適用于標稱型數據,因此數值型數據必須離散化
(3) 分析數據:可以使用任何方法,構造樹完成之后,我們應該檢查圖像是否符合預期
(4) 訓練算法:構造樹的數據結構
(5) 測試算法:使用經驗樹計算錯誤率
(6)使用算法:此步驟可以適用于任何監督學習算法,而使用決策樹可以更好的理解數據的內在含義
1.2 劃分特征
一些決策樹算法采用二分法劃分數據,此處不采用這種方法,本次將使用ID3算法劃分數據集,該算法處理如何劃分數據集,何時停止劃分數據集。如果依據某個屬性劃分數據將會產生4個可能的值,我們將數據劃分為四塊,并創建四個不同的分支,每次劃分數據集時我們只選取一個特征屬性,如果訓練集中存在20個特征,第一次我們選擇哪個特征作為劃分的參考屬性呢?
下表的數據包含5個海洋動物,特征包括:不浮出水面是否可以生存,以及是否有腳趾。我們可以將這些動物劃分為兩類:魚類和非魚類,現在我們想要決定依據第一個特征還是第二個特征劃分數據,那么那個特征可以作為第一個劃分點呢?讓我們繼續學習。
1.3 信息增益
劃分數據集的大原則是:將無序的數據變得更加有序。我們可以使用多種方法劃分數據集,但是每種方法都有各自的優缺點。組織雜亂無章數據的一種方法就是使用信息論度量信息,信息論是量化處理信息的分支科學。我們可以在劃分數據之前使用信息論量化度量信息的內容。
在劃分數據集之前之后信息發生的變化成為信息增益,知道如何計算信息增益,我們就可以計算每個特征值劃分數據集獲得的信息增益,獲得信息增益最高的特征就是最好的選擇。
在可以評測哪種數據劃分方式是最好的數據劃分之前,我們必須學習如何計算信息增益。集合信息的度量方式稱為香農熵或者簡稱為熵(這名字來源于信息論之父克勞德*香農)
熵定義為信息的期望值,在明白這個概念之前,我們必須知道信息的定義,如果待分類的事務可能劃分在多個分類之中,則符號Xi的信息定義為:
其中P(Xi)是選擇該分類的概率。
為了計算熵,我們需要計算所有類別所有可能值包含的信息期望值,通過下面的公式得到:
其中n是分類的數目。
代碼如下:
此代碼是使用Python計算給定數據集的信息熵。
from math import log
# 計算數據的熵(entropy)
def calcShannonRnt(dataSet):
# 數據條數,計算數據集中實例的總數
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet:
# 每行數據的最后一個類別(也就是標簽)
currentLable = featVec[-1]
if currentLable not in labelCounts.keys():
labelCounts[currentLable] = 0
# 統計有多少個類以及每個類的數量
labelCounts[currentLable] += 1
shannonEnt = 0.0
for key in labelCounts:
# 計算單個類的熵值
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
代碼執行結果:
myData,labels = createDataSet() print(myData) print(labels) shannonEnt = calcShannonRnt(myData) print(shannonEnt) ''' [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] ['no surfacing', 'flippers'] 0.9709505944546686 '''
從結果來看,熵為0.97,熵越高,則混合的數據也越多,隨機變量的不確定性越大,我們可以在數據集中添加更多的分類,觀察熵是如何變化的。
2,劃分數據集
上面我們學習了如何度量數據集的無序程度,分類算法除了需要測量信息熵,還需要劃分數據集,度量劃分數據集的熵,以便判斷當前是否正確地劃分了數據集,我們將對每個特征劃分數據集的結果計算一次信息熵,然后判斷按照哪個特征劃分數據集是最好的劃分方式。想象一個分布在二維空間的數據散點圖,需要在數據之間畫條線,將他們分成兩部分,我們應該按照x軸還是y軸劃分呢?
按照給定特征劃分數據集代碼:
# 按照給定特征劃分數據集
def splitDataSet(dataSet,axis,value):
'''
:param dataSet: 待劃分的數據集
:param axis: 劃分數據集的特征
:param value: 特征的返回值
:return:
'''
retDataSet = []
for featVec in dataSet:
# 如果發現符合要求的特征,將其添加到新創建的列表中
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
我們使用劃分數據集測試一下,可以看到劃分數據集結果如下:
myData,labels = createDataSet() print(myData) print(labels) splitDataSetVal = splitDataSet(myData,0,1) print(splitDataSetVal) splitDataSetVal = splitDataSet(myData,0,0) print(splitDataSetVal) ''' [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] ['no surfacing', 'flippers'] [[1, 'yes'], [1, 'yes'], [0, 'no']] [[1, 'no'], [1, 'no']] '''
接下來,我們將遍歷整個數據集,循環計算香農熵和splitDataSet()函數,找到最好的特征劃分方式,熵計算將會告訴我們如何劃分數據集是最好的數據組織方式。
選擇最好的數據集劃分方式代碼:
# 選擇最好的數據集劃分方式
def chooseBestFeatureTpSplit(dataSet):
'''
此函數中調用的數據滿足以下要求
1,數據必須是一種由列表元素組成的列表,而且所有列表元素都要具有相同的數據長度
2,數據的最后一列或者實例的最后一個元素是當前實例的類別標簽
:param dataSet:
:return:
'''
numFeatures = len(dataSet[0]) - 1
# 原始的熵
baseEntropy = calcShannonRnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
# 創建唯一的分類標簽列表
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
# 計算每種劃分方式的信息熵,并對所有唯一特征值得到的熵求和
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
# 按照特征分類后的熵
newEntropy += prob * calcShannonRnt(subDataSet)
infoGain = baseEntropy - newEntropy
if (infoGain > bestInfoGain):
# 計算最好的信息增益,信息增益越大,區分樣本的能力越強,根據代表性
bestInfoGain = infoGain
bestFeature = i
return bestFeature
測試代碼,得到結果:
myData,labels = createDataSet() print(myData) print(labels) bestFeatureRes = chooseBestFeatureTpSplit(myData) print(bestFeatureRes) ''' [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] ['no surfacing', 'flippers'] 0 '''
結果告訴我們第0個特征是最好的用于劃分數據集的特征。如果按此特征分類,第一個海洋動物分組將有兩個屬于魚類,兩個屬于非魚類,另一個分組則只有一個非魚類。
假設我們按照第一個特征屬性劃分數據,也就是說第一個特征是1的放在一個組,第一個特征是0的放在另一個組,數據一致性如何?按照上述的方法劃分數據集,第一個特征為1的海洋生物分組將有兩個屬于魚類,一個屬于非魚類;另一個分組則全部屬于非魚類。
3 遞歸構建決策樹
總結一下構建決策樹的工作原理:得到原始數據集,然后基于最好的屬性值劃分數據集,由于特征值可能多于兩個,因此可能存在大于兩個分支的數據集劃分。第一次劃分之后,數據將向下傳遞到樹分支的下一個節點,在這個節點上,我們可以再次劃分數據。因此我們可以采用遞歸的原則處理數據集。
遞歸結束的條件是:程序遍歷完所有劃分數據集的屬性,或者每個分支下的所有實例都具有相同的分類。如果所有實例具有相同的分類,則得到一個葉子節點護著終止塊。任何到達葉子節點的數據必然屬于葉子節點的分類,參見下圖:
第一個結束條件使得算法可以終止,我們甚至可以設置算法可以劃分的最大分組數目。當然目前我們只需要在算法開始運行前計算列的數目,查看算法是否使用了所有屬性即可,如果數據集已經處理了所有屬性,但是類標簽依然不是唯一的,此時我們需要決定如何定義該葉子節點,在這種情況下,我們通常會采用多數表決的方法決定該葉子節點的分類。
按照分類后類別數量排序代碼:
# 按照分類后類別數量排序
def majorityCnt(classList):
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):
'''
:param dataSet: 輸入的數據集
:param labels: 標簽列表(包含了數據集中所有特征的標簽)
:return:
'''
# classList 包含了數據集中所有類的標簽
classList = [example[-1] for example in dataSet]
# 類別完全相同則停止繼續劃分
if classList.count(classList[0]) == len(classList):
return classList[0]
# 遍歷完所有特征時返回出現次數最多的
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureTpSplit(dataSet)
bestFeatLabel = labels[bestFeat]
# 字典myTree存儲了樹的所有信息,這對于后面繪制樹形圖很重要
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
# 得到列表包含的所有屬性值
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels =labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),
subLabels)
運行代碼結果如下:
myData,labels = createDataSet()
print(myData)
print(labels)
myTree = createTree(myData,labels)
print(myTree)
'''
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
['no surfacing', 'flippers']
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
'''
從結果來看,變量myTree包含了很多代表樹結構信息的嵌套字典,從左邊開始,第一次關鍵字nosurfacing是第一個劃分數據集的特征名稱,該關鍵字的值也是另一個數據字典,第二個關鍵字是no surfacing特征劃分的數據集,這些關鍵字的值時no surfacing節點的子節點,這些值可能是類標簽,也可能是另一個數據字典,如果值時類標簽,則該子節點是葉子節點;如果值時另一個數據字典,則子節點是一個判斷節點,這種格式結構不斷重復就構成了整棵樹。
4 使用Matplotlib注解繪制樹形圖
上面我們學習了如何從數據集中創建樹,然而字典的表示形式非常不易于理解,而且直接繪制圖形也比較困難,這里我們學習使用Matplotlib庫創建樹形圖。決策樹的主要優點就是直觀易于理解,如果不能將其直觀的顯示出來,就無法發揮其優勢。
Matplotlib可以對文字著色并提供多種形狀以供選擇,而且我們還可以反轉箭頭,將它指向文本框而不是數據點。
使用文本注解繪制樹節點代碼:
import matplotlib.pyplot as plt
# 定義文本框和箭頭格式(樹節點格式的常量)
decisionNode = dict(boxstyle='sawtooth',fc='0.8')
leafNode = dict(boxstyle='round4',fc='0.8')
arrows_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=arrows_args)
def createPlot():
# 創建一個新圖形并清空繪圖區
fig = plt.figure(1,facecolor='white')
fig.clf()
createPlot.ax1 = plt.subplot(111,frameon=False)
plotNode('a decision node',(0.5,0.1),(0.1,0.5),decisionNode)
plotNode('a leaf node',(0.8,0.1),(0.3,0.8),leafNode)
plt.show()
if __name__ == '__main__':
createPlot()
結果:
5 構造決策樹
繪制一顆完整的樹需要一些技巧。我們雖然有x,y坐標,但是如何放置所有的樹節點卻是個問題,我們必須知道有多少個葉子節點,以便可以正確確定x軸的長度我們還需要知道樹有多少層,以便可以正確確定y軸的高度,這里我們定義兩個新函數getNumLeafs() 和getTreeDepth(),來獲取葉節點數目和樹的層數,如下:
獲取葉子節點的數目和樹的層數代碼:
import matplotlib.pyplot as plt
def myTree():
treeData = {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
return treeData
# 獲取葉子節點的數目
def getNumLeafs(myTree):
# 初始化結點數
numLeafs = 0
firstSides = list(myTree.keys())
# 找到輸入的第一個元素,第一個關鍵詞為劃分數據集類別的標簽
firstStr = firstSides[0]
secondDect = myTree[firstStr]
for key in secondDect.keys():
# 測試節點的數據類型是否為字典
if type(secondDect[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDect[key])
else:
numLeafs +=1
return numLeafs
# 獲取樹的層數
def getTreeDepth(myTree):
maxDepth = 0
firstSides = list(myTree.keys())
firstStr = firstSides[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
if __name__ == '__main__':
myData = myTree()
LeafNum = getNumLeafs(myData)
TreeDepth = getTreeDepth(myData)
print(LeafNum)
print(TreeDepth)
測試結果如下:
3 2
下面我們將前面的方法組合在一起,得到決策樹,此時我們需要更新畫圖的代碼:
完整代碼如下:
import matplotlib.pyplot as plt
# 定義文本框和箭頭格式(樹節點格式的常量)
decisionNode = dict(boxstyle='sawtooth',fc='0.8')
leafNode = dict(boxstyle='round4',fc='0.8')
arrows_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=arrows_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(myData)
depth = getTreeDepth(myData)
firstStides = list(myTree.keys())
firstStr = firstStides[0]
# 按照葉子結點個數劃分x軸
cntrPt = (plotTree.xOff + (0.1 + float(numLeafs)) /2.0/plotTree.totalW,plotTree.yOff)
plotMidText(cntrPt,parentPt,nodeTxt)
plotNode(firstStr,cntrPt,parentPt,decisionNode)
secondDict = myTree[firstStr]
# y方向上的擺放位置 自上而下繪制,因此遞減y值
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 # x方向計算結點坐標
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 # 下次重新調用時恢復y
def myTree():
treeData = {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
return treeData
# 獲取葉子節點的數目
def getNumLeafs(myTree):
# 初始化結點數
numLeafs = 0
firstSides = list(myTree.keys())
# 找到輸入的第一個元素,第一個關鍵詞為劃分數據集類別的標簽
firstStr = firstSides[0]
secondDect = myTree[firstStr]
for key in secondDect.keys():
# 測試節點的數據類型是否為字典
if type(secondDect[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDect[key])
else:
numLeafs +=1
return numLeafs
# 獲取樹的層數
def getTreeDepth(myTree):
maxDepth = 0
firstSides = list(myTree.keys())
firstStr = firstSides[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
# 主函數
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))
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
if __name__ == '__main__':
myData = myTree()
myData['no surfacing'][3] = 'maybe'
print(myData)
# LeafNum = getNumLeafs(myData)
# TreeDepth = getTreeDepth(myData)
# print(LeafNum)
# print(TreeDepth)
createPlot(myData)
6 測試算法:使用決策樹執行分類
依靠訓練數據構造了決策樹之后,我們可以將它用于實際數據的分類,在執行數據分類時,需要決策樹以及用于構造樹的標簽向量。然后,程序比較測試數據與決策樹上的數值,遞歸執行該過程直到進入葉子節點,最后將測試數據定義為葉子節點所屬的類型。
使用決策樹的分類函數代碼:
from math import log
import operator
# 計算數據的熵(entropy)
def calcShannonRnt(dataSet):
# 數據條數,計算數據集中實例的總數
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet:
# 每行數據的最后一個類別(也就是標簽)
currentLable = featVec[-1]
if currentLable not in labelCounts.keys():
labelCounts[currentLable] = 0
# 統計有多少個類以及每個類的數量
labelCounts[currentLable] += 1
shannonEnt = 0.0
for key in labelCounts:
# 計算單個類的熵值
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']
# dataSet = [['Long', 'Think', 'male'],
# ['Short', 'Think', 'male'],
# ['Short', 'Think', 'male'],
# ['Long', 'Thin', 'female'],
# ['Short', 'Thin', 'female'],
# ['Short', 'Think', 'female'],
# ['Long', 'Think', 'female'],
# ['Long', 'Think', 'female']]
# labels = ['hair', 'voice']
return dataSet,labels
# 按照給定特征劃分數據集
def splitDataSet(dataSet,axis,value):
'''
:param dataSet: 待劃分的數據集
:param axis: 劃分數據集的特征
:param value: 特征的返回值
:return:
'''
retDataSet = []
for featVec in dataSet:
# 如果發現符合要求的特征,將其添加到新創建的列表中
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
# 選擇最好的數據集劃分方式
def chooseBestFeatureTpSplit(dataSet):
'''
此函數中調用的數據滿足以下要求
1,數據必須是一種由列表元素組成的列表,而且所有列表元素都要具有相同的數據長度
2,數據的最后一列或者實例的最后一個元素是當前實例的類別標簽
:param dataSet:
:return:
'''
numFeatures = len(dataSet[0]) - 1
# 原始的熵
baseEntropy = calcShannonRnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
# 創建唯一的分類標簽列表
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
# 計算每種劃分方式的信息熵,并對所有唯一特征值得到的熵求和
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
# 按照特征分類后的熵
newEntropy += prob * calcShannonRnt(subDataSet)
infoGain = baseEntropy - newEntropy
if (infoGain > bestInfoGain):
# 計算最好的信息增益,信息增益越大,區分樣本的能力越強,根據代表性
bestInfoGain = infoGain
bestFeature = i
return bestFeature
# 按照分類后類別數量排序
def majorityCnt(classList):
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):
'''
:param dataSet: 輸入的數據集
:param labels: 標簽列表(包含了數據集中所有特征的標簽)
:return:
'''
# classList 包含了數據集中所有類的標簽
classList = [example[-1] for example in dataSet]
# 類別完全相同則停止繼續劃分
if classList.count(classList[0]) == len(classList):
return classList[0]
# 遍歷完所有特征時返回出現次數最多的
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureTpSplit(dataSet)
bestFeatLabel = labels[bestFeat]
# 字典myTree存儲了樹的所有信息,這對于后面繪制樹形圖很重要
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
# 得到列表包含的所有屬性值
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels =labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),
subLabels)
return myTree
# 使用決策樹的分類函數
def classify(inputTree ,featLabels,testVec):
firstSides = list(inputTree.keys())
firstStr = firstSides[0]
secondDict = inputTree[firstStr]
# print('featLables:',featLabels)
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
# 若該特征值等于當前key,yes往下走
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key],featLabels,testVec)
else:
classLabel = secondDict[key]
return classLabel
#使用決策樹來分類
def classify11(inputTree,featLabels,testVec):
#python3.X
firstSides = list(inputTree.keys())
firstStr = firstSides[0] # 找到輸入的第一個元素
# python3.X
secondDict=inputTree[firstStr] #baocun在secondDict中
featIndex=featLabels.index(firstStr) #建立索引
for key in secondDict.keys():
if testVec[featIndex]==key: #若該特征值等于當前key,yes往下走
if type(secondDict[key]).__name__=='dict':# 若為樹結構
classLabel=classify(secondDict[key],featLabels,testVec) #遞歸調用
else: classLabel=secondDict[key]#為葉子結點,賦予label值
return classLabel #分類結果
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]
測試結果如下:
if __name__ == '__main__':
myData,labels = createDataSet()
# print(myData)
# print(labels)
myTree = createTree(myData,labels)
# print(myTree0)
myTree1 = {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
# print(type(myTree1))
# myTree = retrieveTree(0)
myData, labels = createDataSet()
print(myData)
print(labels)
res1 = classify(myTree,labels,[1,1])
print(res1)
res2 = classify(myTree,labels,[1,0])
print(res2)
'''
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
['no surfacing', 'flippers']
yes
no
'''
7 使用算法:決策樹的存儲
構造決策樹是很耗時的任務,即使處理很小的數據集,如前面的樣本數據,也要花費幾秒的時間,如果數據集很大,將會耗費很多計算時間,然而使用創建好的決策樹解決問題,則可以很快完成。所以為了節省時間,最好能夠在每次執行分類時調用已經構造好的決策樹,為了解決這個問題,需要使用pickle序列化對象。序列化對象可以在磁盤上保存對象,并在需要的時候讀取出來,任何對象都可以執行序列化操作,字典對象也不例外。
使用pickle模塊存儲決策樹代碼:
# 使用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)
通過上面的代碼,我們可以將分類器存儲在硬盤上,而不是每次對數據分類時重新學習一邊,這就是決策樹的優點之一。
驗證上面代碼,測試結果如下:
if __name__ == '__main__':
myData,labels = createDataSet()
myTree = createTree(myData,labels)
print(myTree)
print(type(myTree))
myTree1 = {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
storeTree(myTree1,'classifierStorage.txt')
res = grabTree('classifierStorage.txt')
print(res)
'''
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
<class 'dict'>
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
'''
8 示例:使用決策樹預測隱形眼鏡類型
此處通過一個例子學習決策樹如何預測患者需要佩戴的隱形眼鏡類型,使用小數據集,我們就可以利用決策樹學到很多知識:眼科醫生是如何判斷患者需要佩戴的鏡片類型;
(隱形眼鏡數據集時非常著名的數據集,它包含很多患者眼部狀況的觀察條件以及醫生推薦的隱形眼鏡類型,隱形眼鏡類型包括硬材質,軟材質以及不適合佩戴隱形眼鏡。數據來源于UCI數據庫,為了更容易顯示數據,機器學習實戰將數據做了簡單的更改,數據存儲在文本文件中)
lenses.txt內容可以去我的GitHub上拿(GitHub地址:https://github.com/LeBron-Jian/MachineLearningNote)
程序如下:
# _*_coding:utf-8_*_
import operator
from math import log
import matplotlib.pyplot as plt
# 計算香農熵 度量數據集無序程度
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for fearVec in dataSet:
currentLabel = fearVec[-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 splitDataSet(dataSet,axis,value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducesFeatVec = featVec[:axis]
reducesFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducesFeatVec)
return retDataSet
# 選擇最好的數據集劃分方式
def chooseBestFeatureTpSplit(dataSet):
'''
此函數中調用的數據滿足以下要求
1,數據必須是一種由列表元素組成的列表,而且所有列表元素都要具有相同的數據長度
2,數據的最后一列或者實例的最后一個元素是當前實例的類別標簽
:param dataSet:
:return:
'''
numFeatures = len(dataSet[0]) - 1
# 原始的熵
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
# 創建唯一的分類標簽列表
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
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
if (infoGain > bestInfoGain):
# 計算最好的信息增益,信息增益越大,區分樣本的能力越強,根據代表性
bestInfoGain = infoGain
bestFeature = i
return bestFeature
# 按照分類后類別數量排序
def majorityCnt(classList):
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):
'''
:param dataSet: 輸入的數據集
:param labels: 標簽列表(包含了數據集中所有特征的標簽)
:return:
'''
# classList 包含了數據集中所有類的標簽
classList = [example[-1] for example in dataSet]
# 類別完全相同則停止繼續劃分
if classList.count(classList[0]) == len(classList):
return classList[0]
# 遍歷完所有特征時返回出現次數最多的
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureTpSplit(dataSet)
bestFeatLabel = labels[bestFeat]
# 字典myTree存儲了樹的所有信息,這對于后面繪制樹形圖很重要
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
# 得到列表包含的所有屬性值
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels =labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),
subLabels)
return myTree
# 獲取葉子節點的數目
def getNumLeafs(myTree):
# 初始化結點數
numLeafs = 0
firstSides = list(myTree.keys())
# 找到輸入的第一個元素,第一個關鍵詞為劃分數據集類別的標簽
firstStr = firstSides[0]
secondDect = myTree[firstStr]
for key in secondDect.keys():
# 測試節點的數據類型是否為字典
if type(secondDect[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDect[key])
else:
numLeafs +=1
return numLeafs
# 獲取樹的層數
def getTreeDepth(myTree):
maxDepth = 0
firstSides = list(myTree.keys())
firstStr = firstSides[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
# 定義文本框和箭頭格式(樹節點格式的常量)
decisionNode = dict(boxstyle='sawtooth',fc='0.8')
leafNode = dict(boxstyle='round4',fc='0.8')
arrows_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=arrows_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(myData)
depth = getTreeDepth(myData)
firstStides = list(myTree.keys())
firstStr = firstStides[0]
# 按照葉子結點個數劃分x軸
cntrPt = (plotTree.xOff + (0.1 + float(numLeafs)) /2.0/plotTree.totalW,plotTree.yOff)
plotMidText(cntrPt,parentPt,nodeTxt)
plotNode(firstStr,cntrPt,parentPt,decisionNode)
secondDict = myTree[firstStr]
# y方向上的擺放位置 自上而下繪制,因此遞減y值
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 # x方向計算結點坐標
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 # 下次重新調用時恢復y
# 主函數
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))
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
if __name__ == '__main__':
fr = open('lenses.txt')
lenses = [inst.strip().split(' ') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate']
print(lenses)
myData = createTree(lenses, lensesLabels)
print(myData)
createPlot(myData)
結果如下:
上圖所示的決策樹非常好的匹配了實驗數據,然而這些選項可能太多了。我們將這種問題稱為過度匹配,為了減少過度匹配問題,我們可以裁剪決策樹,去掉一些不必要的葉子節點,如果葉子節點只能增加少許信息,則可以刪除該節點,將它并入到其他葉子節點中。
ID3算法無法直接處理數值型數據,盡管我們可以通過量化的方法將數值型數據轉化為標稱型數值,但是如果存在太多的特征劃分,ID3算法仍然會面臨其他問題。
9 總結
決策樹分類器就像帶有終止塊的流程圖,終止塊表示分類結果。開始處理數據集時,我們首先需要測量集合中數據的不一致性,也就是熵,然后尋找最優方案劃分數據集,直到數據集中的所有數據屬于同一分類。ID3算法可以用于劃分標稱型數據集。構建決策樹時,我們通常采取遞歸的方法將數據集轉化為決策樹。一般我們并不構造新的數據結構,而是使用字典存儲樹節點信息。
完整代碼及其數據,請移步小編的GitHub
傳送門:請點擊我
如果點擊有誤:https://github.com/LeBron-Jian/MachineLearningNote
參考文獻:機器學習實戰
總結
以上是生活随笔為你收集整理的python机器学习笔记:ID3决策树算法实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redux的工作流程
- 下一篇: 获取手机运营商