機器學習決策樹演算法實戰:理論 + 詳細的 Python 3 程式碼實現
國慶長假已過近半,想想當初自己立下的學習計劃 Flag 究竟有沒有完成?一篇長文乾貨值得你收藏!
作者簡介:莫塵,學生一枚,努力學習機器學習,深度學習的相關知識,目前正在研究自然語言處理方向。文字選自莫塵的CSDN部落格。
一、前言
本篇討論決策樹的原理和決策樹構建的準備工作,機器學習決策樹的原理,以及如何選擇最優特徵作為分類特徵,決策樹構建,決策樹視覺化,使用決策樹進行分類預測,決策樹的儲存和讀取以及sklearn實戰之預測隱形眼睛型別。
本文出現的所有程式碼,均可在github上下載,歡迎Follow、Star:Github地址: https://github.com/yaoguangju/machine_learning
二、決策樹的基礎
1、決策樹是什麼
決策樹是什麼?決策樹(decision tree)是一種基本的分類與迴歸方法。舉個通俗易懂的例子,如下圖所示的流程圖就是一個決策樹,長方形代表判斷模組(decision block),橢圓形成代表終止模組(terminating block),表示已經得出結論,可以終止執行。從判斷模組引出的左右箭頭稱作為分支(branch),它可以達到另一個判斷模組或者終止模組。我們還可以這樣理解,分類決策樹模型是一種描述對例項進行分類的樹形結構。決策樹由結點(node)和有向邊(directed edge)組成。結點有兩種型別:內部結點(internal node)和葉結點(leaf node)。內部結點表示一個特徵或屬性,葉結點表示一個類。蒙圈沒??
如下圖所示的決策樹,長方形和橢圓形都是結點。長方形的結點屬於內部結點,橢圓形的結點屬於葉結點,從結點引出的左右箭頭就是有向邊。而最上面的結點就是決策樹的根結點(root node)。這樣,結點說法就與模組說法對應上了。
我們回到這個流程圖,對,你沒看錯,這就是一個假想的相親物件分類系統。它首先檢測相親對方是否有房。如果有房,則對於這個相親物件可以考慮進一步接觸。如果沒有房,則觀察相親物件是否有上進心,如果沒有,直接Say Goodbye,此時可以說:”你人很好,但是我們不合適。”如果有,同樣也值得認真考慮。
不過這只是個簡單的相親物件分類系統,只是做了簡單的分類。真實情況可能要複雜得多,考慮因素也可以是五花八門。脾氣好嗎?會做飯嗎?願意做家務嗎?家裡幾個孩子?父母是幹什麼的?等等各種因素。
我們可以把決策樹看成一個if-then規則的集合,將決策樹轉換成if-then規則的過程是這樣的:由決策樹的根結點(root node)到葉結點(leaf node)的每一條路徑構建一條規則;路徑上內部結點的特徵對應著規則的條件,而葉結點的類對應著規則的結論。決策樹的路徑或其對應的if-then規則集合具有一個重要的性質:互斥並且完備。這就是說,每一個例項都被一條路徑或一條規則所覆蓋,而且只被一條路徑或一條規則所覆蓋。這裡所覆蓋是指例項的特徵與路徑上的特徵一致或例項滿足規則的條件。
使用決策樹做預測需要以下過程:
-
收集資料:可以使用任何方法。比如想構建一個相親系統,我們可以從媒婆那裡,或者通過採訪相親物件獲取資料。根據他們考慮的因素和最終的選擇結果,就可以得到一些供我們利用的資料了。
-
準備資料:收集完的資料,我們要進行整理,將這些所有收集的資訊按照一定規則整理出來,並排版,方便我們進行後續處理。
-
分析資料:可以使用任何方法,決策樹構造完成之後,我們可以檢查決策樹圖形是否符合預期。
-
訓練演算法:這個過程也就是構造決策樹,同樣也可以說是決策樹學習,就是構造一個決策樹的資料結構。
-
測試演算法:使用經驗樹計算錯誤率。當錯誤率達到了可接收範圍,這個決策樹就可以投放使用了。
-
使用演算法:此步驟可以使用適用於任何監督學習演算法,而使用決策樹可以更好地理解資料的內在含義。
2、決策樹的構建的準備工作
使用決策樹做預測的每一步驟都很重要,資料收集不到位,將會導致沒有足夠的特徵讓我們構建錯誤率低的決策樹。資料特徵充足,但是不知道用哪些特徵好,將會導致無法構建出分類效果好的決策樹模型。從演算法方面看,決策樹的構建是我們的核心內容。
決策樹要如何構建呢?通常,這一過程可以概括為3個步驟: 特徵選擇、決策樹的生成和決策樹的修剪 。
-
特徵選擇
特徵選擇在於選取對訓練資料具有分類能力的特徵。這樣可以提高決策樹學習的效率,如果利用一個特徵進行分類的結果與隨機分類的結果沒有很大差別,則稱這個特徵是沒有分類能力的。經驗上扔掉這樣的特徵對決策樹學習的精度影響不大。通常特徵選擇的標準是資訊增益(information gain)或資訊增益比,為了簡單,本文使用資訊增益作為選擇特徵的標準。那麼,什麼是資訊增益?在講解資訊增益之前,讓我們看一組例項,貸款申請樣本資料表。
希望通過所給的訓練資料學習一個貸款申請的決策樹,用於對未來的貸款申請進行分類,即當新的客戶提出貸款申請時,根據申請人的特徵利用決策樹決定是否批准貸款申請。
特徵選擇就是決定用哪個特徵來劃分特徵空間。比如,我們通過上述資料表得到兩個可能的決策樹,分別由兩個不同特徵的根結點構成。
圖(a)所示的根結點的特徵是年齡,有3個取值,對應於不同的取值有不同的子結點。圖(b)所示的根節點的特徵是工作,有2個取值,對應於不同的取值有不同的子結點。兩個決策樹都可以從此延續下去。問題是:究竟選擇哪個特徵更好些?這就要求確定選擇特徵的準則。直觀上,如果一個特徵具有更好的分類能力,或者說,按照這一特徵將訓練資料集分割成子集,使得各個子集在當前條件下有最好的分類,那麼就更應該選擇這個特徵。資訊增益就能夠很好地表示這一直觀的準則。
什麼是資訊增益呢? 在劃分資料集之後資訊發生的變化稱為資訊增益,知道如何計算資訊增益,我們就可以計算每個特徵值劃分資料集獲得的資訊增益,獲得資訊增益最高的特徵就是最好的選擇。
(1)夏農熵
在可以評測哪個資料劃分方式是最好的資料劃分之前,我們必須學習如何計算資訊增益。集合資訊的度量方式成為夏農熵或者簡稱為熵(entropy),這個名字來源於資訊理論之父克勞德·夏農。
如果看不明白什麼是資訊增益和熵,請不要著急,因為他們自誕生的那一天起,就註定會令世人十分費解。克勞德·夏農寫完資訊理論之後,約翰·馮·諾依曼建議使用”熵”這個術語,因為大家都不知道它是什麼意思。
熵定義為資訊的期望值。在資訊理論與概率統計中,熵是表示隨機變數不確定性的度量。如果待分類的事物可能劃分在多個分類之中,則符號xi的資訊定義為 :
其中p(xi)是選擇該分類的概率。有人可能會問,資訊為啥這樣定義啊?答曰:前輩得出的結論。這就跟1+1等於2一樣,記住並且會用即可。上述式中的對數以2為底,也可以e為底(自然對數)。
通過上式,我們可以得到所有類別的資訊。為了計算熵,我們需要計算所有類別所有可能值包含的資訊期望值(數學期望),通過下面的公式得到:
其中n是分類的數目。熵越大,隨機變數的不確定性就越大。
當熵中的概率由資料估計(特別是最大似然估計)得到時,所對應的熵稱為經驗熵(empirical entropy)。什麼叫由資料估計?比如有10個數據,一共有兩個類別,A類和B類。其中有7個數據屬於A類,則該A類的概率即為十分之七。其中有3個數據屬於B類,則該B類的概率即為十分之三。淺顯的解釋就是,這概率是我們根據資料數出來的。我們定義貸款申請樣本資料表中的資料為訓練資料集D,則訓練資料集D的經驗熵為H(D),|D|表示其樣本容量,及樣本個數。設有K個類Ck, = 1,2,3,…,K,|Ck|為屬於類Ck的樣本個數,因此經驗熵公式就可以寫為 :
根據此公式計算經驗熵H(D),分析貸款申請樣本資料表中的資料。最終分類結果只有兩類,即放貸和不放貸。根據表中的資料統計可知,在15個數據中,9個數據的結果為放貸,6個數據的結果為不放貸。所以資料集D的經驗熵H(D)為:
經過計算可知,資料集D的經驗熵H(D)的值為0.971。
(2)編寫程式碼計算經驗熵
在編寫程式碼之前,我們先對資料集進行屬性標註。
-
年齡:0代表青年,1代表中年,2代表老年;
-
有工作:0代表否,1代表是;
-
有自己的房子:0代表否,1代表是;
-
信貸情況:0代表一般,1代表好,2代表非常好;
-
類別(是否給貸款):no代表否,yes代表是。
確定這些之後,我們就可以建立資料集,並計算經驗熵了,程式碼編寫如下:
程式碼執行結果如下圖所示,程式碼是先列印訓練資料集,然後列印計算的經驗熵H(D),程式計算的結果與我們統計計算的結果是一致的,程式沒有問題。
(3) 資訊增益
在上面,我們已經說過,如何選擇特徵,需要看資訊增益。也就是說,資訊增益是相對於特徵而言的,資訊增益越大,特徵對最終的分類結果影響也就越大,我們就應該選擇對最終分類結果影響最大的那個特徵作為我們的分類特徵。
在講解資訊增益定義之前,我們還需要明確一個概念,條件熵。
熵我們知道是什麼,條件熵又是個什麼鬼?條件熵H(Y|X)表示在已知隨機變數X的條件下隨機變數Y的不確定性,隨機變數X給定的條件下隨機變數Y的條件熵(conditional entropy)H(Y|X),定義為X給定條件下Y的條件概率分佈的熵對X的數學期望:
這裡,
同理,當條件熵中的概率由資料估計(特別是極大似然估計)得到時,所對應的條件熵成為條件經驗熵(empirical conditional entropy)。
明確了條件熵和經驗條件熵的概念。接下來,讓我們說說資訊增益。前面也提到了,資訊增益是相對於特徵而言的。所以,特徵A對訓練資料集D的資訊增益g(D,A),定義為集合D的經驗熵H(D)與特徵A給定條件下D的經驗條件熵H(D|A)之差,即:
一般地,熵H(D)與條件熵H(D|A)之差成為互資訊(mutual information)。決策樹學習中的資訊增益等價於訓練資料集中類與特徵的互資訊。
設特徵A有n個不同的取值{a1,a2,···,an},根據特徵A的取值將D劃分為n個子集{D1,D2,···,Dn},|Di|為Di的樣本個數。記子集Di中屬於Ck的樣本的集合為Dik,即Dik = Di ∩ Ck,|Dik|為Dik的樣本個數。於是經驗條件熵的公式可以些為:
說了這麼多概念性的東西,沒有聽懂也沒有關係,舉幾個例子,再回來看一下概念,就懂了。
以貸款申請樣本資料表為例進行說明。看下年齡這一列的資料,也就是特徵A1,一共有三個類別,分別是:青年、中年和老年。我們只看年齡是青年的資料,年齡是青年的資料一共有5個,所以年齡是青年的資料在訓練資料集出現的概率是十五分之五,也就是三分之一。同理,年齡是中年和老年的資料在訓練資料集出現的概率也都是三分之一。現在我們只看年齡是青年的資料的最終得到貸款的概率為五分之二,因為在五個資料中,只有兩個資料顯示拿到了最終的貸款,同理,年齡是中年和老年的資料最終得到貸款的概率分別為五分之三、五分之四。所以計算年齡的資訊增益,過程如下:
同理,計算其餘特徵的資訊增益g(D,A2)、g(D,A3)和g(D,A4)。分別為:
最後,比較特徵的資訊增益,由於特徵A3(有自己的房子)的資訊增益值最大,所以選擇A3作為最優特徵。
(4) 編寫程式碼計算資訊增益
我們已經學會了通過公式計算資訊增益,接下來編寫程式碼,計算資訊增益。
# -*- coding: UTF-8 -*- from math import log """ 函式說明:計算給定資料集的經驗熵(夏農熵) Parameters: dataSet - 資料集 Returns: shannonEnt - 經驗熵(夏農熵) """ def calcShannonEnt(dataSet): numEntires = len(dataSet) #返回資料集的行數 labelCounts = {} #儲存每個標籤(Label)出現次數的字典 for featVec in dataSet: #對每組特徵向量進行統計 currentLabel = featVec[-1] #提取標籤(Label)資訊 if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去 labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 #Label計數 shannonEnt = 0.0 #經驗熵(夏農熵) for key in labelCounts: #計算夏農熵 prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率 shannonEnt -= prob * log(prob, 2) #利用公式計算 return shannonEnt #返回經驗熵(夏農熵) """ 函式說明:建立測試資料集 Parameters: 無 Returns: dataSet - 資料集 labels - 特徵標籤 """ def createDataSet(): dataSet = [[0, 0, 0, 0, 'no'], #資料集 [0, 0, 0, 1, 'no'], [0, 1, 0, 1, 'yes'], [0, 1, 1, 0, 'yes'], [0, 0, 0, 0, 'no'], [1, 0, 0, 0, 'no'], [1, 0, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [1, 0, 1, 2, 'yes'], [1, 0, 1, 2, 'yes'], [2, 0, 1, 2, 'yes'], [2, 0, 1, 1, 'yes'], [2, 1, 0, 1, 'yes'], [2, 1, 0, 2, 'yes'], [2, 0, 0, 0, 'no']] labels = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特徵標籤 return dataSet, labels #返回資料集和分類屬性 """ 函式說明:選擇最優特徵 Parameters: dataSet - 資料集 Returns: bestFeature - 資訊增益最大的(最優)特徵的索引值 """ def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 #特徵數量 baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵 bestInfoGain = 0.0 #資訊增益 bestFeature = -1 #最優特徵的索引值 for i in range(numFeatures): #遍歷所有特徵 #獲取dataSet的第i個所有特徵 featList = [example[i] for example in dataSet] uniqueVals = set(featList) #建立set集合{},元素不可重複 newEntropy = 0.0 #經驗條件熵 for value in uniqueVals: #計算資訊增益 subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集 prob = len(subDataSet) / float(len(dataSet)) #計運算元集的概率 newEntropy += prob * calcShannonEnt(subDataSet) #根據公式計算經驗條件熵 infoGain = baseEntropy - newEntropy #資訊增益 print("第%d個特徵的增益為%.3f" % (i, infoGain)) #列印每個特徵的資訊增益 if (infoGain > bestInfoGain): #計算資訊增益 bestInfoGain = infoGain #更新資訊增益,找到最大的資訊增益 bestFeature = i #記錄資訊增益最大的特徵的索引值 return bestFeature #返回資訊增益最大的特徵的索引值 if __name__ == '__main__': dataSet, features = createDataSet() print("最優特徵索引值:" + str(chooseBestFeatureToSplit(dataSet)))
splitDataSet函式是用來選擇各個特徵的子集的,比如選擇年齡(第0個特徵)的青年(用0代表)的自己,我們可以呼叫splitDataSet(dataSet,0,0)這樣返回的子集就是年齡為青年的5個數據集。chooseBestFeatureToSplit是選擇選擇最優特徵的函式。執行程式碼結果如下:
對比我們自己計算的結果,發現結果完全正確!最優特徵的索引值為2,也就是特徵A3(有自己的房子)。
決策樹生成和修剪
我們已經學習了從資料集構造決策樹演算法所需要的子功能模組,包括經驗熵的計算和最優特徵的選擇,其工作原理如下:得到原始資料集,然後基於最好的屬性值劃分資料集,由於特徵值可能多於兩個,因此可能存在大於兩個分支的資料集劃分。第一次劃分之後,資料集被向下傳遞到樹的分支的下一個結點。在這個結點上,我們可以再次劃分資料。因此我們可以採用遞迴的原則處理資料集。
構建決策樹的演算法有很多,比如C4.5、ID3和CART,這些演算法在執行時並不總是在每次劃分資料分組時都會消耗特徵。由於特徵數目並不是每次劃分資料分組時都減少,因此這些演算法在實際使用時可能引起一定的問題。目前我們並不需要考慮這個問題,只需要在演算法開始執行前計算列的數目,檢視演算法是否使用了所有屬性即可。
決策樹生成演算法遞迴地產生決策樹,直到不能繼續下去未為止。這樣產生的樹往往對訓練資料的分類很準確,但對未知的測試資料的分類卻沒有那麼準確,即出現過擬合現象。過擬合的原因在於學習時過多地考慮如何提高對訓練資料的正確分類,從而構建出過於複雜的決策樹。解決這個問題的辦法是考慮決策樹的複雜度,對已生成的決策樹進行簡化。
三、決策樹構建
上篇文章也粗略提到過,構建決策樹的演算法有很多。篇幅原因,本篇文章只使用ID3演算法構建決策樹。
1、ID3演算法
ID3演算法的核心是在決策樹各個結點上對應資訊增益準則選擇特徵,遞迴地構建決策樹。具體方法是:從根結點(root node)開始,對結點計算所有可能的特徵的資訊增益,選擇資訊增益最大的特徵作為結點的特徵,由該特徵的不同取值建立子節點;再對子結點遞迴地呼叫以上方法,構建決策樹;直到所有特徵的資訊增益均很小或沒有特徵可以選擇為止。最後得到一個決策樹。ID3相當於用極大似然法進行概率模型的選擇。
在使用ID3構造決策樹之前,我們再分析下資料。
利用上篇文章求得的結果,由於特徵A3(有自己的房子)的資訊增益值最大,所以選擇特徵A3作為根結點的特徵。它將訓練集D劃分為兩個子集D1(A3取值為”是”)和D2(A3取值為”否”)。由於D1只有同一類的樣本點,所以它成為一個葉結點,結點的類標記為“是”。
對D2則需要從特徵A1(年齡),A2(有工作)和A4(信貸情況)中選擇新的特徵,計算各個特徵的資訊增益:
根據計算,選擇資訊增益最大的特徵A2(有工作)作為結點的特徵。由於A2有兩個可能取值,從這一結點引出兩個子結點:一個對應”是”(有工作)的子結點,包含3個樣本,它們屬於同一類,所以這是一個葉結點,類標記為”是”;另一個是對應”否”(無工作)的子結點,包含6個樣本,它們也屬於同一類,所以這也是一個葉結點,類標記為”否”。
這樣就生成了一個決策樹,該決策樹只用了兩個特徵(有兩個內部結點),生成的決策樹如下圖所示。
這樣我們就使用ID3演算法構建出來了決策樹,接下來,讓我們看看如何進行代實現。
2、編寫程式碼構建決策樹
我們使用字典儲存決策樹的結構,比如上小節我們分析出來的決策樹,用字典可以表示為:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
建立函式majorityCnt統計classList中出現此處最多的元素(類標籤),建立函式createTree用來遞迴構建決策樹。編寫程式碼如下:
# -*- coding: UTF-8 -*- from math import log import operator """ 函式說明:計算給定資料集的經驗熵(夏農熵) Parameters: dataSet - 資料集 Returns: shannonEnt - 經驗熵(夏農熵) """ def calcShannonEnt(dataSet): numEntires = len(dataSet) #返回資料集的行數 labelCounts = {} #儲存每個標籤(Label)出現次數的字典 for featVec in dataSet: #對每組特徵向量進行統計 currentLabel = featVec[-1] #提取標籤(Label)資訊 if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去 labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 #Label計數 shannonEnt = 0.0 #經驗熵(夏農熵) for key in labelCounts: #計算夏農熵 prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率 shannonEnt -= prob * log(prob, 2) #利用公式計算 return shannonEnt #返回經驗熵(夏農熵) """ 函式說明:建立測試資料集 Parameters: 無 Returns: dataSet - 資料集 labels - 特徵標籤 """ def createDataSet(): dataSet = [[0, 0, 0, 0, 'no'], #資料集 [0, 0, 0, 1, 'no'], [0, 1, 0, 1, 'yes'], [0, 1, 1, 0, 'yes'], [0, 0, 0, 0, 'no'], [1, 0, 0, 0, 'no'], [1, 0, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [1, 0, 1, 2, 'yes'], [1, 0, 1, 2, 'yes'], [2, 0, 1, 2, 'yes'], [2, 0, 1, 1, 'yes'], [2, 1, 0, 1, 'yes'], [2, 1, 0, 2, 'yes'], [2, 0, 0, 0, 'no']] labels = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特徵標籤 return dataSet, labels #返回資料集和分類屬性 """ 函式說明:按照給定特徵劃分資料集 Parameters: dataSet - 待劃分的資料集 axis - 劃分資料集的特徵 value - 需要返回的特徵的值 Returns: 無 """ def splitDataSet(dataSet, axis, value): retDataSet = [] #建立返回的資料集列表 for featVec in dataSet: #遍歷資料集 if featVec[axis] == value: reducedFeatVec = featVec[:axis] #去掉axis特徵 reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的新增到返回的資料集 retDataSet.append(reducedFeatVec) return retDataSet #返回劃分後的資料集 """ 函式說明:選擇最優特徵 Parameters: dataSet - 資料集 Returns: bestFeature - 資訊增益最大的(最優)特徵的索引值 """ def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 #特徵數量 baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵 bestInfoGain = 0.0 #資訊增益 bestFeature = -1 #最優特徵的索引值 for i in range(numFeatures): #遍歷所有特徵 #獲取dataSet的第i個所有特徵 featList = [example[i] for example in dataSet] uniqueVals = set(featList) #建立set集合{},元素不可重複 newEntropy = 0.0 #經驗條件熵 for value in uniqueVals: #計算資訊增益 subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集 prob = len(subDataSet) / float(len(dataSet)) #計運算元集的概率 newEntropy += prob * calcShannonEnt(subDataSet) #根據公式計算經驗條件熵 infoGain = baseEntropy - newEntropy #資訊增益 # print("第%d個特徵的增益為%.3f" % (i, infoGain)) #列印每個特徵的資訊增益 if (infoGain > bestInfoGain): #計算資訊增益 bestInfoGain = infoGain #更新資訊增益,找到最大的資訊增益 bestFeature = i #記錄資訊增益最大的特徵的索引值 return bestFeature #返回資訊增益最大的特徵的索引值 """ 函式說明:統計classList中出現此處最多的元素(類標籤) Parameters: classList - 類標籤列表 Returns: sortedClassCount[0][0] - 出現此處最多的元素(類標籤) """ def majorityCnt(classList): classCount = {} for vote in classList: #統計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] #返回classList中出現次數最多的元素 """ 函式說明:建立決策樹 Parameters: dataSet - 訓練資料集 labels - 分類屬性標籤 featLabels - 儲存選擇的最優特徵標籤 Returns: myTree - 決策樹 """ def createTree(dataSet, labels, featLabels): classList = [example[-1] for example in dataSet] #取分類標籤(是否放貸:yes or no) if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續劃分 return classList[0] if len(dataSet[0]) == 1: #遍歷完所有特徵時返回出現次數最多的類標籤 return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優特徵 bestFeatLabel = labels[bestFeat] #最優特徵的標籤 featLabels.append(bestFeatLabel) myTree = {bestFeatLabel:{}} #根據最優特徵的標籤生成樹 del(labels[bestFeat]) #刪除已經使用特徵標籤 featValues = [example[bestFeat] for example in dataSet] #得到訓練集中所有最優特徵的屬性值 uniqueVals = set(featValues) #去掉重複的屬性值 for value in uniqueVals: #遍歷特徵,建立決策樹。 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels) return myTree if __name__ == '__main__': dataSet, labels = createDataSet() featLabels = [] myTree = createTree(dataSet, labels, featLabels) print(myTree)
遞迴建立決策樹時,遞迴有兩個終止條件:第一個停止條件是所有的類標籤完全相同,則直接返回該類標籤;第二個停止條件是使用完了所有特徵,仍然不能將資料劃分僅包含唯一類別的分組,即決策樹構建失敗,特徵不夠用。此時說明資料緯度不夠,由於第二個停止條件無法簡單地返回唯一的類標籤,這裡挑選出現數量最多的類別作為返回值。
執行上述程式碼,我們可以看到如下結果:
可見,我們的決策樹已經構建完成了。這時候,有的朋友可能會說,這個決策樹看著好彆扭,雖然這個能看懂,但是如果多點的結點,就不好看了。能直觀點嗎?完全沒有問題,我們可以使用強大的Matplotlib繪製決策樹。
3、決策樹視覺化
這裡程式碼都是關於Matplotlib的,如果對於Matplotlib不瞭解的,可以先學習下,Matplotlib的內容這裡就不再累述。視覺化需要用到的函式:
-
getNumLeafs:獲取決策樹葉子結點的數目
-
getTreeDepth:獲取決策樹的層數
-
plotNode:繪製結點
-
plotMidText:標註有向邊屬性值
-
plotTree:繪製決策樹
-
createPlot:建立繪製面板
對視覺化決策樹的程式進行了詳細的註釋,直接看程式碼,除錯檢視即可。為了顯示中文,需要設定FontProperties,程式碼編寫如下:
(點選圖片檢視大圖)
不出意外的話,我們就可以得到如下結果,可以看到決策樹繪製完成。plotNode函式的工作就是繪製各個結點,比如有自己的房子、有工作、yes、no,包括內結點和葉子結點。plotMidText函式的工作就是繪製各個有向邊的屬性。
4、使用決策樹執行分類
依靠訓練資料構造了決策樹之後,我們可以將它用於實際資料的分類。在執行資料分類時,需要決策樹以及用於構造樹的標籤向量。然後,程式比較測試資料與決策樹上的數值,遞迴執行該過程直到進入葉子結點;最後將測試資料定義為葉子結點所屬的型別。在構建決策樹的程式碼,可以看到,有個featLabels引數。它是用來幹什麼的?它就是用來記錄各個分類結點的,在用決策樹做預測的時候,我們按順序輸入需要的分類結點的屬性值即可。舉個例子,比如我用上述已經訓練好的決策樹做分類,那麼我只需要提供這個人是否有房子,是否有工作這兩個資訊即可,無需提供冗餘的資訊。
用決策樹做分類的程式碼很簡單,編寫程式碼如下:
(點選圖片,檢視大圖)
這裡只增加了classify函式,用於決策樹分類。輸入測試資料[0,1],它代表沒有房子,但是有工作,分類結果如下所示:
看到這裡,細心的朋友可能就會問了,每次做預測都要訓練一次決策樹?這也太麻煩了吧?有什麼好的解決嗎?
5、決策樹的儲存
構造決策樹是很耗時的任務,即使處理很小的資料集,如前面的樣本資料,也要花費幾秒的時間,如果資料集很大,將會耗費很多計算時間。然而用建立好的決策樹解決分類問題,則可以很快完成。因此,為了節省計算時間,最好能夠在每次執行分類時呼叫已經構造好的決策樹。為了解決這個問題,需要使用Python模組pickle序列化物件。序列化物件可以在磁碟上儲存物件,並在需要的時候讀取出來。
假設我們已經得到決策樹
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
使用pickle.dump儲存決策樹。
# -*- coding: UTF-8 -*- import pickle """ 函式說明:儲存決策樹 Parameters: inputTree - 已經生成的決策樹 filename - 決策樹的儲存檔名 Returns: 無 """ def storeTree(inputTree, filename): with open(filename, 'wb') as fw: pickle.dump(inputTree, fw) if __name__ == '__main__': myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}} storeTree(myTree, 'classifierStorage.txt')
執行程式碼,在該Python檔案的相同目錄下,會生成一個名為classifierStorage.txt的txt檔案,這個檔案二進位制儲存著我們的決策樹。我們可以使用VScode開啟看下儲存結果。
看不懂?沒錯,因為這個是個二進位制儲存的檔案,我們也無需看懂裡面的內容,會儲存,會用即可。那麼問題來了。將決策樹儲存完這個二進位制檔案,然後下次使用的話,怎麼用呢?
很簡單使用pickle.load進行載入即可,編寫程式碼如下:
# -*- coding: UTF-8 -*- import pickle """ 函式說明:讀取決策樹 Parameters: filename - 決策樹的儲存檔名 Returns: pickle.load(fr) - 決策樹字典 """ def grabTree(filename): fr = open(filename, 'rb') return pickle.load(fr) if __name__ == '__main__': myTree = grabTree('classifierStorage.txt') print(myTree)
如果在該Python檔案的相同目錄下,有一個名為classifierStorage.txt的檔案,那麼我們就可以執行上述程式碼,執行結果如下圖所示:
從上述結果中,我們可以看到,我們順利載入了儲存決策樹的二進位制檔案。
四、Sklearn之使用決策樹預測隱形眼睛型別
1、實戰背景
進入本文的正題:眼科醫生是如何判斷患者需要佩戴隱形眼鏡的型別的?一旦理解了決策樹的工作原理,我們甚至也可以幫助人們判斷需要佩戴的鏡片型別。
隱形眼鏡資料集是非常著名的資料集,它包含很多換著眼部狀態的觀察條件以及醫生推薦的隱形眼鏡型別。隱形眼鏡型別包括硬材質(hard)、軟材質(soft)以及不適合佩戴隱形眼鏡(no lenses)。資料來源與UCI資料庫,資料集下載地址:點選進入連結
一共有24組資料,資料的Labels依次是age、prescript、astigmatic、tearRate、class,也就是第一列是年齡,第二列是症狀,第三列是是否散光,第四列是眼淚數量,第五列是最終的分類標籤。資料如下圖所示:
可以使用已經寫好的Python程式構建決策樹,不過出於繼續學習的目的,本文使用Sklearn實現。
2、使用Sklearn構建決策樹
官方英文文件地址:
http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
sklearn.tree模組提供了決策樹模型,用於解決分類問題和迴歸問題。方法如下圖所示:
本次實戰內容使用的是DecisionTreeClassifier和export_graphviz,前者用於決策樹構建,後者用於決策樹視覺化。
DecisionTreeClassifier構建決策樹:
讓我們先看下DecisionTreeClassifier這個函式,一共有12個引數:
引數說明如下:
-
criterion:特徵選擇標準,可選引數,預設是gini,可以設定為entropy。gini是基尼不純度,是將來自集合的某種結果隨機應用於某一資料項的預期誤差率,是一種基於統計的思想。entropy是夏農熵,也就是上篇文章講過的內容,是一種基於資訊理論的思想。Sklearn把gini設為預設引數,應該也是做了相應的斟酌的,精度也許更高些?ID3演算法使用的是entropy,CART演算法使用的則是gini。
-
splitter:特徵劃分點選擇標準,可選引數,預設是best,可以設定為random。每個結點的選擇策略。best引數是根據演算法選擇最佳的切分特徵,例如gini、entropy。random隨機的在部分劃分點中找區域性最優的劃分點。預設的”best”適合樣本量不大的時候,而如果樣本資料量非常大,此時決策樹構建推薦”random”。
-
max_features:劃分時考慮的最大特徵數,可選引數,預設是None。尋找最佳切分時考慮的最大特徵數(n_features為總共的特徵數),有如下6種情況:
-
如果max_features是整型的數,則考慮max_features個特徵;
-
如果max_features是浮點型的數,則考慮int(max_features * n_features)個特徵;
-
如果max_features設為auto,那麼max_features = sqrt(n_features);
-
如果max_features設為sqrt,那麼max_featrues = sqrt(n_features),跟auto一樣;
-
如果max_features設為log2,那麼max_features = log2(n_features);
-
如果max_features設為None,那麼max_features = n_features,也就是所有特徵都用。
-
一般來說,如果樣本特徵數不多,比如小於50,我們用預設的”None”就可以了,如果特徵數非常多,我們可以靈活使用剛才描述的其他取值來控制劃分時考慮的最大特徵數,以控制決策樹的生成時間。
-
max_depth:決策樹最大深,可選引數,預設是None。這個引數是這是樹的層數的。層數的概念就是,比如在貸款的例子中,決策樹的層數是2層。如果這個引數設定為None,那麼決策樹在建立子樹的時候不會限制子樹的深度。一般來說,資料少或者特徵少的時候可以不管這個值。或者如果設定了min_samples_slipt引數,那麼直到少於- - - min_smaples_split個樣本為止。如果模型樣本量多,特徵也多的情況下,推薦限制這個最大深度,具體的取值取決於資料的分佈。常用的可以取值10-100之間。
-
min_samples_split:內部節點再劃分所需最小樣本數,可選引數,預設是2。這個值限制了子樹繼續劃分的條件。如果min_samples_split為整數,那麼在切分內部結點的時候,min_samples_split作為最小的樣本數,也就是說,如果樣本已經少於min_samples_split個樣本,則停止繼續切分。如果min_samples_split為浮點數,那麼min_samples_split就是一個百分比,ceil(min_samples_split * n_samples),數是向上取整的。如果樣本量不大,不需要管這個值。如果樣本量數量級非常大,則推薦增大這個值。
-
min_samples_leaf:葉子節點最少樣本數,可選引數,預設是1。這個值限制了葉子節點最少的樣本數,如果某葉子節點數目小於樣本數,則會和兄弟節點一起被剪枝。葉結點需要最少的樣本數,也就是最後到葉結點,需要多少個樣本才能算一個葉結點。如果設定為1,哪怕這個類別只有1個樣本,決策樹也會構建出來。如果min_samples_leaf是整數,那麼min_samples_leaf作為最小的樣本數。如果是浮點數,那麼min_samples_leaf就是一個百分比,同上,celi(min_samples_leaf * n_samples),數是向上取整的。如果樣本量不大,不需要管這個值。如果樣本量數量級非常大,則推薦增大這個值。
-
min_weight_fraction_leaf:葉子節點最小的樣本權重和,可選引數,預設是0。這個值限制了葉子節點所有樣本權重和的最小值,如果小於這個值,則會和兄弟節點一起被剪枝。一般來說,如果我們有較多樣本有缺失值,或者分類樹樣本的分佈類別偏差很大,就會引入樣本權重,這時我們就要注意這個值了。
-
max_leaf_nodes:最大葉子節點數,可選引數,預設是None。通過限制最大葉子節點數,可以防止過擬合。如果加了限制,演算法會建立在最大葉子節點數內最優的決策樹。如果特徵不多,可以不考慮這個值,但是如果特徵分成多的話,可以加以限制,具體的值可以通過交叉驗證得到。
-
class_weight:類別權重,可選引數,預設是None,也可以字典、字典列表、balanced。指定樣本各類別的的權重,主要是為了防止訓練集某些類別的樣本過多,導致訓練的決策樹過於偏向這些類別。類別的權重可以通過{class_label:weight}這樣的格式給出,這裡可以自己指定各個樣本的權重,或者用balanced,如果使用balanced,則演算法會自己計算權重,樣本量少的類別所對應的樣本權重會高。當然,如果你的樣本類別分佈沒有明顯的偏倚,則可以不管這個引數,選擇預設的None。
-
random_state:可選引數,預設是None。隨機數種子。如果是證書,那麼random_state會作為隨機數生成器的隨機數種子。隨機數種子,如果沒有設定隨機數,隨機出來的數與當前系統時間有關,每個時刻都是不同的。如果設定了隨機數種子,那麼相同隨機數種子,不同時刻產生的隨機數也是相同的。如果是RandomState instance,那麼random_state是隨機數生成器。如果為None,則隨機數生成器使用np.random。
-
min_impurity_split:節點劃分最小不純度,可選引數,預設是1e-7。這是個閾值,這個值限制了決策樹的增長,如果某節點的不純度(基尼係數,資訊增益,均方差,絕對差)小於這個閾值,則該節點不再生成子節點。即為葉子節點 。
-
presort:資料是否預排序,可選引數,預設為False,這個值是布林值,預設是False不排序。一般來說,如果樣本量少或者限制了一個深度很小的決策樹,設定為true可以讓劃分點選擇更加快,決策樹建立的更加快。如果樣本量太大的話,反而沒有什麼好處。問題是樣本量少的時候,我速度本來就不慢。所以這個值一般懶得理它就可以了。
除了這些引數要注意以外,其他在調參時的注意點有:
-
當樣本數量少但是樣本特徵非常多的時候,決策樹很容易過擬合,一般來說,樣本數比特徵數多一些會比較容易建立健壯的模型
-
如果樣本數量少但是樣本特徵非常多,在擬合決策樹模型前,推薦先做維度規約,比如主成分分析(PCA),特徵選擇(Losso)或者獨立成分分析(ICA)。這樣特徵的維度會大大減小。再來擬合決策樹模型效果會好。
-
推薦多用決策樹的視覺化,同時先限制決策樹的深度,這樣可以先觀察下生成的決策樹裡資料的初步擬合情況,然後再決定是否要增加深度。
-
在訓練模型時,注意觀察樣本的類別情況(主要指分類樹),如果類別分佈非常不均勻,就要考慮用class_weight來限制模型過於偏向樣本多的類別。
-
決策樹的陣列使用的是numpy的float32型別,如果訓練資料不是這樣的格式,演算法會先做copy再執行。
-
如果輸入的樣本矩陣是稀疏的,推薦在擬合前呼叫csc_matrix稀疏化,在預測前呼叫csr_matrix稀疏化。
sklearn.tree.DecisionTreeClassifier()提供了一些方法供我們使用,如下圖所示:
瞭解到這些,我們就可以編寫程式碼了。
注意 一點,由於fit()函式不能接收string型別的資料,通過列印的資訊可以看到,資料都是string型別的。在使用fit()函式之前,我們需要對資料集進行編碼,這裡可以使用兩種方法:
-
LabelEncoder :將字串轉換為增量值
-
OneHotEncoder:使用One-of-K演算法將字串轉換為整數
為了對string型別的資料序列化,需要先生成pandas資料,這樣方便我們的序列化工作。這裡我使用的方法是,原始資料->字典->pandas資料,編寫程式碼如下:
import pandas as pd if __name__ == '__main__': with open('lenses.txt', 'r') as fr: # 載入檔案 lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 處理檔案 lenses_target = [] # 提取每組資料的類別,儲存在列表裡 for each in lenses: lenses_target.append(each[-1]) lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 特徵標籤 lenses_list = [] # 儲存lenses資料的臨時列表 lenses_dict = {} # 儲存lenses資料的字典,用於生成pandas for each_label in lensesLabels: # 提取資訊,生成字典 for each in lenses: lenses_list.append(each[lensesLabels.index(each_label)]) lenses_dict[each_label] = lenses_list lenses_list = [] print(lenses_dict) # 列印字典資訊 lenses_pd = pd.DataFrame(lenses_dict) # 生成pandas.DataFrame print(lenses_pd)
從執行結果可以看出,順利生成pandas資料。
接下來,將資料序列化,編寫程式碼如下:
# -*- coding: UTF-8 -*- import pandas as pd from sklearn.preprocessing import LabelEncoder import pydotplus from sklearn.externals.six import StringIO if __name__ == '__main__': with open('lenses.txt', 'r') as fr: #載入檔案 lenses = [inst.strip().split('\t') for inst in fr.readlines()] #處理檔案 lenses_target = [] #提取每組資料的類別,儲存在列表裡 for each in lenses: lenses_target.append(each[-1]) lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] #特徵標籤 lenses_list = [] #儲存lenses資料的臨時列表 lenses_dict = {} #儲存lenses資料的字典,用於生成pandas for each_label in lensesLabels: #提取資訊,生成字典 for each in lenses: lenses_list.append(each[lensesLabels.index(each_label)]) lenses_dict[each_label] = lenses_list lenses_list = [] # print(lenses_dict) #列印字典資訊 lenses_pd = pd.DataFrame(lenses_dict) #生成pandas.DataFrame print(lenses_pd) #列印pandas.DataFrame le = LabelEncoder() #建立LabelEncoder()物件,用於序列化 for col in lenses_pd.columns: #為每一列序列化 lenses_pd[col] = le.fit_transform(lenses_pd[col]) print(lenses_pd)
從列印結果可以看到,我們已經將資料順利序列化,接下來。我們就可以fit()資料,構建決策樹了。
3、使用Graphviz視覺化決策樹
Graphviz的是AT&T Labs Research開發的圖形繪製工具,他可以很方便的用來繪製結構化的圖形網路,支援多種格式輸出,生成圖片的質量和速度都不錯。它的輸入是一個用dot語言編寫的繪圖指令碼,通過對輸入指令碼的解析,分析出其中的點,邊以及子圖,然後根據屬性進行繪製。是使用Sklearn生成的決策樹就是dot格式的,因此我們可以直接利用Graphviz將決策樹視覺化。
在講解編寫程式碼之前,我們需要安裝兩樣東西,即pydotplus和Grphviz。
(1)安裝Pydotplus
pydotplus可以在CMD視窗中,直接使用指令安裝:
pip install pydotplus
(2)安裝Graphviz
Graphviz不能使用pip進行安裝,我們需要手動安裝,下載地址:http://www.graphviz.org/Home.php
下載好安裝包,進行安裝,安裝完畢之後,需要設定Graphviz的環境變數。
首先,按快捷鍵win+r,在出現的執行對話方塊中輸入sysdm.cpl,點選確定,出現如下對話方塊:
選擇高階->環境變數。在系統變數的Path變數中,新增Graphviz的環境變數,比如Graphviz安裝在了D盤的根目錄,則新增:D:\Graphviz\bin;
新增好環境變數之後,我們就可以正常使用Graphviz了。
(3)編寫程式碼
程式碼如下,視覺化部分的程式碼不難,都是有套路的,直接填引數就好,詳細內容可以檢視官方教程:http://scikit-learn.org/stable/modules/tree.html#tree
執行程式碼,在該python檔案儲存的相同目錄下,會生成一個名為tree的PDF檔案,開啟檔案,我們就可以看到決策樹的視覺化效果圖了。
確定好決策樹之後,我們就可以做預測了。可以根據自己的眼睛情況和年齡等特徵,看一看自己適合何種材質的隱形眼鏡。使用如下程式碼就可以看到預測結果:
print(clf.predict([[1,1,1,0]])) #預測
程式碼簡單,官方手冊都有,就不全貼出來了。
五、總結
決策樹的一些優點:
-
易於理解和解釋。決策樹可以視覺化。
-
幾乎不需要資料預處理。其他方法經常需要資料標準化,建立虛擬變數和刪除缺失值。決策樹還不支援缺失值。
-
使用樹的花費(例如預測資料)是訓練資料點(data points)數量的對數。
-
可以同時處理數值變數和分類變數。其他方法大都適用於分析一種變數的集合。
-
可以處理多值輸出變數問題。
-
使用白盒模型。如果一個情況被觀察到,使用邏輯判斷容易表示這種規則。相反,如果是黑盒模型(例如人工神經網路),結果會非常難解釋。
-
即使對真實模型來說,假設無效的情況下,也可以較好的適用。
決策樹的一些缺點:
-
決策樹學習可能建立一個過於複雜的樹,並不能很好的預測資料。也就是過擬合。修剪機制(現在不支援),設定一個葉子節點需要的最小樣本數量,或者數的最大深度,可以避免過擬合。
-
決策樹可能是不穩定的,因為即使非常小的變異,可能會產生一顆完全不同的樹。這個問題通過decision trees with an ensemble來緩解。
-
概念難以學習,因為決策樹沒有很好的解釋他們,例如,XOR, parity or multiplexer problems。
-
如果某些分類佔優勢,決策樹將會建立一棵有偏差的樹。因此,建議在訓練之前,先抽樣使樣本均衡。
其他:
本書參考《機器學習實戰》下載地址:機器學習實戰
如有問題,請留言。如有錯誤,還望指正,謝謝!
原文地址:
https://blog.csdn.net/ling_mochen/article/details/80011263
碼了這麼多字,留個贊鼓勵下~~
推薦閱讀: ofollow,noindex">查什麼攻略?百行 Python 程式碼告訴你國慶哪些景點爆滿!
2018 AI開發者大會
◆
只講技術,拒絕空談
◆
2018 AI開發者大會首輪重磅嘉賓及深度議題現已火熱出爐,掃碼搶“鮮”看。國慶特惠,購票立享 5 折優惠!