2020.4.5添加:
哈哈,遲來的源碼,我把它放到GitHub上了:包含詳細註釋的樹模型源碼;包括決策樹和隨機森林,歡迎取用,歡迎討論,歡迎star;
我才發現CSDN的資源下載自動要求這麼多積分,我之前上傳的時候是限定0積分的。。
###################正文分割線######################
決策樹其實就是按節點分類數據集的一種方法。在本文中,我將討論數學上如何使用信息論劃分數據集,並編寫代碼構建決策樹(本文使用ID3算法構建決策樹,ID3算法可以用來劃分標稱型數據集)。
創建決策樹進行分類的流程如下:
(1) 創建數據集
(2) 計算數據集的信息熵
(3) 遍歷所有特徵,選擇信息熵最小的特徵,即爲最好的分類特徵
(4) 根據上一步得到的分類特徵分割數據集,並將該特徵從列表中移除
(5) 執行遞歸函數,返回第三步,不斷分割數據集,直到分類結束
(6) 使用決策樹執行分類,返回分類結果
首先,給出一個簡單數據集:
數據解讀:
在該數據集中包含五個海洋動物,有兩個特徵:(1)不浮出水面是否可以生存;(2)是否有腳蹼;這些動物被分成兩類:魚類和非魚類。在我們構建決策樹的過程中,對某個動物,只有兩個特徵都爲“是”時,纔將其判定爲魚類。
在構建決策樹時,我們需要解決的第一個問題是:當前數據集哪個特徵在劃分數據分類時起決定性作用,即我們要如何找出最優的分類特徵。爲了找到決定性的特徵,劃分出最好的結果,我們必須評估每個特徵。完成數據劃分後,原始數據集就被劃分爲幾個數據子集,這些數據子集會分佈在第一個決策點的所有分支上。如果某個分支下的數據屬於同一類型,即數據已正確分類,無需進一步分割。如果數據子集內的數據不屬於同個類型,則需要重複劃分數據子集的過程。劃分數據子集的算法和劃分原始數據集的方法相同(因此可用遞歸函數繼續劃分子集),直到所有具有相同類型的數據都在一個數據子集內。
構建決策樹的僞代碼函數createTree()如下所示:
檢測數據集中的每個子集是否屬於同一分類:
If so return 類標籤
Else:
尋找劃分數據集的最好特徵
劃分數據集
創建分支節點
For 每個劃分的子集:
調用函數createTree()並增加返回結果到分支節點中
Return 分支節點
1)劃分數據集的大原則是:將無序的數據變得更加有序。這裏引入信息熵的概念。如果待分類的事務可能劃分在多個分類之中,則符號xi的信息定義爲:
其中p(xi)是選擇該分類的概率。
爲了計算熵,我們需要計算所有類別所有可能值包含的信息期望值,通過下面的公式得到:
直觀的理解:如果x屬於某個分類的值越大(即數據越有序),H的值越小;極端情況下,p(xi)=1時,H=0,此時分類最準確。所以我們要使H的值儘可能小。
計算給定數據集的信息熵的代碼如下:
'''計算數據集的信息熵 (信息熵即指類別標籤的混亂程度,值越小越好)'''
def calcshan(dataSet):
lenDataSet=len(dataSet)
p={}
H=0.0
for data in dataSet:
currentLabel=data[-1] #獲取類別標籤
if currentLabel not in p.keys(): #若字典中不存在該類別標籤,即創建
p[currentLabel]=0
p[currentLabel]+=1 #遞增類別標籤的值
for key in p:
px=float(p[key])/float(lenDataSet) #計算某個標籤的概率
H-=px*log(px,2) #計算信息熵
return H
結果如下:
(2)計算完信息熵後,我們便可以得到數據集的無序程度。我們將對每個特徵劃分數據集的結果計算一次信息熵,然後判斷哪個特徵劃分數據集是最好的劃分方式(根據信息熵判斷,信息熵越小,說明劃分效果越好)。
按照給定特徵劃分數據集的代碼如下:
'''根據某一特徵分類數據集'''
def spiltData(dataSet,axis,value): #dataSet爲要劃分的數據集,axis爲給定的特徵,value爲給定特徵的具體值
subDataSet=[]
for data in dataSet:
subData=[]
if data[axis]==value:
subData=data[:axis] #取出data中第0到axis-1個數進subData;
subData.extend(data[axis+1:]) #取出data中第axis+1到最後一個數進subData;這兩行代碼相當於把第axis個數從數據集中剔除掉
subDataSet.append(subData) #此處要注意expend和append的區別
return subDataSet
結果如下:
(3)選擇最好的數據集劃分方式,代碼如下:
'''遍歷所有特徵,選擇信息熵最小的特徵,即爲最好的分類特徵'''
def chooseBestFeature(dataSet):
lenFeature=len(dataSet[0])-1 #計算特徵維度時要把類別標籤那一列去掉
shanInit=calcshan(dataSet) #計算原始數據集的信息熵
feature=[]
inValue=0.0
bestFeature=0
for i in range(lenFeature):
shanCarry=0.0
feature=[example[i] for example in dataSet] #提取第i個特徵的所有數據
feature=set(feature) #得到第i個特徵所有的分類值,如'0'和'1'
for feat in feature:
subData=spiltData(dataSet,i,feat) #先對數據集按照分類值分類
prob=float(len(subData))/float(len(dataSet))
shanCarry+=prob*calcshan(subData) #計算第i個特徵的信息熵
outValue=shanInit-shanCarry #原始數據信息熵與循環中的信息熵的差
if (outValue>inValue):
inValue=outValue #將信息熵與原始熵相減後的值賦給inValue,方便下一個循環的信息熵差值與其比較
bestFeature=i
return bestFeature
結果如下:
(4)選擇好最好的劃分特徵後,接下來,可以開始創建決策樹了。其工作原理如下:得到原始數據集,然後基於最好的屬性值劃分數據集,由於特徵值可能多於兩個,因此可能存在大於兩個分支的數據集劃分。第一次劃分後,數據將被向下傳遞到樹分支的下一個節點,在這個節點上,我們可以再次劃分數據。因此我們可以使用遞歸的原則處理數據集。遞歸結束的條件是:程序遍歷完所有劃分數據集的屬性,或者每個分支下的所有實例都具有相同的分類。
具體實現代碼如下:
'''創建我們所要分類的決策樹'''
def createTree(dataSet,label):
classList=[example[-1] for example in dataSet] #classList是指當前數據集的類別標籤
if classList.count(classList[0])==len(classList): #計算classList中某個類別標籤的數量,若只有一類,則數量與它的數據長度相等
return classList[0]
if len(dataSet[0])==1: #當處理完所有特徵而類別標籤還不唯一時起作用
return majorityCnt(classList)
featBest=chooseBestFeature(dataSet) #選擇最好的分類特徵
feature=[example[featBest] for example in dataSet] #接下來使用該分類特徵進行分類
featValue=set(feature) #得到該特徵所有的分類值,如'0'和'1'
newLabel=label[featBest]
del(label[featBest])
Tree={newLabel:{}} #創建一個多重字典,存儲決策樹分類結果
for value in featValue:
subLabel=label[:]
Tree[newLabel][value]=createTree(spiltData(dataSet,featBest,value),subLabel) #遞歸函數使得Tree不斷創建分支,直到分類結束
return Tree
結果如下:
(5)依靠訓練數據構造了決策樹之後,我們可以將它用於實際數據的分類。在執行數據分類時,需要使用決策樹以及用於構造樹的標籤向量。然後,程序比較測試數據與決策樹上的數值,遞歸執行該過程直到進入葉子節點;最後將測試數據定義爲葉子節點所屬的類型。
具體實現代碼如下:
'''使用決策樹執行分類,返回分類結果'''
def classify(tree,label,testVec): #tree爲createTree()函數返回的決策樹;label爲特徵的標籤值;testVec爲測試數據,即所有特徵的具體值構成的向量
firstFeat=tree.keys()[0] #取出tree的第一個鍵
secondDict=tree[firstFeat] #取出tree第一個鍵的值,即tree的第二個字典(包含關係)
labelIndex=label.index(firstFeat) #得到第一個特徵firstFeat在標籤label中的索引
for key in secondDict.keys(): #遍歷第二個字典的鍵
if testVec[labelIndex]==key: #如果第一個特徵的測試值與第二個字典的鍵相等時
if type(secondDict[key]).__name__=='dict': #如果第二個字典的值還是一個字典,說明分類還沒結束,遞歸執行classify函數
classLabel=classify(secondDict[key],label,testVec) #遞歸函數中只有輸入的第一個參數不同,不斷向字典內層滲入
else:
classLabel=secondDict[key] #最後將得到的分類值賦給classLabel輸出
return classLabel
結果如下:
我們可以看到,只有測試數據的兩個特徵都爲1時,纔會輸出‘yes’,判定爲魚類,結果符合我們的實際要求。
現在我們已經創建了使用決策樹的分類器,但是每次使用分類器時,必須重新構造決策樹,而且構造決策樹是很耗時的任務。因此,爲了節省計算時間,最好能夠在每次執行分類時調用已經構造好的決策樹。這裏我們使用Python的pickle模塊序列化對象。序列化對象可以在磁盤上保存對象,並在需要的時候讀取出來。
使用pickle模塊存儲決策樹代碼如下:
'''使用pickle模塊存儲決策樹'''
def storeTree(tree,filename):
import pickle
fw=open(filename,'w')
pickle.dump(tree,fw)
fw.close()
'''打開文件取出決策樹'''
def loadTree(filename):
import pickle
fr=open(filename,'r')
return pickle.load(fr)
結果如下:
執行完storeTree()函數,我們的代碼路徑裏就會多出一個dataTree.txt的文件,保存決策樹內容,以後要使用決策樹進行分類時,使用loadTree()函數直接調用即可。
參考書籍:
《機器學習實戰》 Peter Harrington著 李銳,李鵬,曲亞東,王斌譯