從這一章開始進入正式的算法學習。
首先我們學習經典而有效的分類算法:決策樹分類算法。
1、決策樹算法
決策樹用樹形結構對樣本的屬性進行分類,是最直觀的分類算法,而且也可以用於迴歸。不過對於一些特殊的邏輯分類會有困難。典型的如異或(XOR)邏輯,決策樹並不擅長解決此類問題。
決策樹的構建不是唯一的,遺憾的是最優決策樹的構建屬於NP問題。因此如何構建一棵好的決策樹是研究的重點。
J. Ross Quinlan在1975提出將信息熵的概念引入決策樹的構建,這就是鼎鼎大名的ID3算法。後續的C4.5, C5.0, CART等都是該方法的改進。
熵就是“無序,混亂”的程度。剛接觸這個概念可能會有些迷惑。想快速瞭解如何用信息熵增益劃分屬性,可以參考這位兄弟的文章:http://blog.csdn.net/alvine008/article/details/37760639
如果還不理解,請看下面這個例子。
假設要構建這麼一個自動選好蘋果的決策樹,簡單起見,我只讓他學習下面這4個樣本:
- 樣本 紅 大 好蘋果
- 0 1 1 1
- 1 1 0 1
- 2 0 1 0
- 3 0 0 0
那麼這個樣本在分類前的信息熵就是S = -(1/2 * log(1/2) + 1/2 * log(1/2)) = 1。
信息熵爲1表示當前處於最混亂,最無序的狀態。
本例僅2個屬性。那麼很自然一共就只可能有2棵決策樹,如下圖所示:
顯然左邊先使用A0(紅色)做劃分依據的決策樹要優於右邊用A1(大小)做劃分依據的決策樹。
當然這是直覺的認知。定量的考察,則需要計算每種劃分情況的信息熵增益。
先選A0作劃分,各子節點信息熵計算如下:
0,1葉子節點有2個正例,0個負例。信息熵爲:e1 = -(2/2 * log(2/2) + 0/2 * log(0/2)) = 0。
2,3葉子節點有0個正例,2個負例。信息熵爲:e2 = -(0/2 * log(0/2) + 2/2 * log(2/2)) = 0。
因此選擇A0劃分後的信息熵爲每個子節點的信息熵所佔比重的加權和:E = e1*2/4 + e2*2/4 = 0。
選擇A0做劃分的信息熵增益G(S, A0)=S - E = 1 - 0 = 1.
事實上,決策樹葉子節點表示已經都屬於相同類別,因此信息熵一定爲0。
同樣的,如果先選A1作劃分,各子節點信息熵計算如下:
0,2子節點有1個正例,1個負例。信息熵爲:e1 = -(1/2 * log(1/2) + 1/2 * log(1/2)) = 1。
1,3子節點有1個正例,1個負例。信息熵爲:e2 = -(1/2 * log(1/2) + 1/2 * log(1/2)) = 1。
因此選擇A1劃分後的信息熵爲每個子節點的信息熵所佔比重的加權和:E = e1*2/4 + e2*2/4 = 1。也就是說分了跟沒分一樣!
選擇A1做劃分的信息熵增益G(S, A1)=S - E = 1 - 1 = 0.
因此,每次劃分之前,我們只需要計算出信息熵增益最大的那種劃分即可。
2、數據集
爲方便講解與理解,我們使用如下一個極其簡單的測試數據集:
- 1.5 50 thin
- 1.5 60 fat
- 1.6 40 thin
- 1.6 60 fat
- 1.7 60 thin
- 1.7 80 fat
- 1.8 60 thin
- 1.8 90 fat
- 1.9 70 thin
- 1.9 80 fat
我們的任務就是訓練一個決策樹分類器,輸入身高和體重,分類器能給出這個人是胖子還是瘦子。
(數據是作者主觀臆斷,具有一定邏輯性,但請無視其合理性)
決策樹對於“是非”的二值邏輯的分枝相當自然。而在本數據集中,身高與體重是連續值怎麼辦呢?
雖然麻煩一點,不過這也不是問題,只需要找到將這些連續值劃分爲不同區間的中間點,就轉換成了二值邏輯問題。
本例決策樹的任務是找到身高、體重中的一些臨界值,按照大於或者小於這些臨界值的邏輯將其樣本兩兩分類,自頂向下構建決策樹。
3、Python實現
Python代碼實現如下:
- # -*- coding: utf-8 -*-
- import numpy as np
- import scipy as sp
- from sklearn import tree
- from sklearn.metrics import precision_recall_curve
- from sklearn.metrics import classification_report
- from sklearn.cross_validation import train_test_split
- ''''' 數據讀入 '''
- data = []
- labels = []
- with open("data\\1.txt") as ifile:
- for line in ifile:
- tokens = line.strip().split(' ')
- data.append([float(tk) for tk in tokens[:-1]])
- labels.append(tokens[-1])
- x = np.array(data)
- labels = np.array(labels)
- y = np.zeros(labels.shape)
- ''''' 標籤轉換爲0/1 '''
- y[labels=='fat']=1
- ''''' 拆分訓練數據與測試數據 '''
- x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.2)
- ''''' 使用信息熵作爲劃分標準,對決策樹進行訓練 '''
- clf = tree.DecisionTreeClassifier(criterion='entropy')
- print(clf)
- clf.fit(x_train, y_train)
- ''''' 把決策樹結構寫入文件 '''
- with open("tree.dot", 'w') as f:
- f = tree.export_graphviz(clf, out_file=f)
- ''''' 係數反映每個特徵的影響力。越大表示該特徵在分類中起到的作用越大 '''
- print(clf.feature_importances_)
- '''''測試結果的打印'''
- answer = clf.predict(x_train)
- print(x_train)
- print(answer)
- print(y_train)
- print(np.mean( answer == y_train))
- '''''準確率與召回率'''
- precision, recall, thresholds = precision_recall_curve(y_train, clf.predict(x_train))
- answer = clf.predict_proba(x)[:,1]
- print(classification_report(y, answer, target_names = ['thin', 'fat']))
輸出結果類似如下所示:
[ 0.2488562 0.7511438]
array([[ 1.6, 60. ],
[ 1.7, 60. ],
[ 1.9, 80. ],
[ 1.5, 50. ],
[ 1.6, 40. ],
[ 1.7, 80. ],
[ 1.8, 90. ],
[ 1.5, 60. ]])
array([ 1., 0., 1., 0., 0., 1., 1., 1.])
array([ 1., 0., 1., 0., 0., 1., 1., 1.])
1.0
thin 0.83 1.00 0.91 5
fat 1.00 0.80 0.89 5
avg / total 1.00 1.00 1.00 8
array([ 0., 1., 0., 1., 0., 1., 0., 1., 0., 0.])
array([ 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])
可以看到,對訓練過的數據做測試,準確率是100%。但是最後將所有數據進行測試,會出現1個測試樣本分類錯誤。
說明本例的決策樹對訓練集的規則吸收的很好,但是預測性稍微差點。
這裏有3點需要說明,這在以後的機器學習中都會用到。
1、拆分訓練數據與測試數據。
這樣做是爲了方便做交叉檢驗。交叉檢驗是爲了充分測試分類器的穩定性。
代碼中的0.2表示隨機取20%的數據作爲測試用。其餘80%用於訓練決策樹。
也就是說10個樣本中隨機取8個訓練。本文數據集小,這裏的目的是可以看到由於取的訓練數據隨機,每次構建的決策樹都不一樣。
2、特徵的不同影響因子。
樣本的不同特徵對分類的影響權重差異會很大。分類結束後看看每個樣本對分類的影響度也是很重要的。
在本例中,身高的權重爲0.25,體重爲0.75,可以看到重量的重要性遠遠高於身高。對於胖瘦的判定而言,這也是相當符合邏輯的。
3、準確率與召回率。
這2個值是評判分類準確率的一個重要標準。比如代碼的最後將所有10個樣本輸入分類器進行測試的結果:
測試結果:array([ 0., 1., 0., 1., 0., 1., 0., 1., 0., 0.])
真實結果:array([ 0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])
分爲thin的準確率爲0.83。是因爲分類器分出了6個thin,其中正確的有5個,因此分爲thin的準確率爲5/6=0.83。
分爲thin的召回率爲1.00。是因爲數據集中共有5個thin,而分類器把他們都分對了(雖然把一個fat分成了thin!),召回率5/5=1。
分爲fat的準確率爲1.00。不再贅述。
分爲fat的召回率爲0.80。是因爲數據集中共有5個fat,而分類器只分出了4個(把一個fat分成了thin!),召回率4/5=0.80。
很多時候,尤其是數據分類難度較大的情況,準確率與召回率往往是矛盾的。你可能需要根據你的需要找到最佳的一個平衡點。
比如本例中,你的目標是儘可能保證找出來的胖子是真胖子(準確率),還是保證儘可能找到更多的胖子(召回率)。
代碼還把決策樹的結構寫入了tree.dot中。打開該文件,很容易畫出決策樹,還可以看到決策樹的更多分類信息。
本文的tree.dot如下所示:
- digraph Tree {
- 0 [label="X[1] <= 55.0000\nentropy = 0.954434002925\nsamples = 8", shape="box"] ;
- 1 [label="entropy = 0.0000\nsamples = 2\nvalue = [ 2. 0.]", shape="box"] ;
- 0 -> 1 ;
- 2 [label="X[1] <= 70.0000\nentropy = 0.650022421648\nsamples = 6", shape="box"] ;
- 0 -> 2 ;
- 3 [label="X[0] <= 1.6500\nentropy = 0.918295834054\nsamples = 3", shape="box"] ;
- 2 -> 3 ;
- 4 [label="entropy = 0.0000\nsamples = 2\nvalue = [ 0. 2.]", shape="box"] ;
- 3 -> 4 ;
- 5 [label="entropy = 0.0000\nsamples = 1\nvalue = [ 1. 0.]", shape="box"] ;
- 3 -> 5 ;
- 6 [label="entropy = 0.0000\nsamples = 3\nvalue = [ 0. 3.]", shape="box"] ;
- 2 -> 6 ;
- }