本文結構:
- 是什么?
- 有什么算法?
- 數學原理?
- 編碼實現算法?
1. 是什么?
簡單地理解,就是根據一些 feature 進行分類,每個節點提一個問題,通過判斷,將數據分為幾類,再繼續提問。這些問題是根據已有數據學習出來的,再投入新數據的時候,就可以根據這棵樹上的問題,將數據划分到合適的葉子上。

2. 有什么算法?
常用的幾種決策樹算法有ID3、C4.5、CART:
ID3:選擇信息熵增益最大的feature作為node,實現對數據的歸納分類。
C4.5:是ID3的一個改進,比ID3准確率高且快,可以處理連續值和有缺失值的feature。
CART:使用基尼指數的划分准則,通過在每個步驟最大限度降低不純潔度,CART能夠處理孤立點以及能夠對空缺值進行處理。
3. 數學原理?
ID3: Iterative Dichotomiser 3

下面這個數據集,可以同時被上面兩顆樹表示,結果是一樣的,而我們更傾向於選擇簡單的樹。
那么怎樣做才能使得學習到的樹是最簡單的呢?

下面是 ID3( Iterative Dichotomiser 3 )的算法:

例如下面數據集,哪個是最好的 Attribute?

用熵Entropy來衡量:
E(S) 是數據集S的熵
i 指每個結果,即 No,Yes的概率

E越大意味着信息越混亂,我們的目標是要讓E最小。
E在0-1之間,如果P+的概率在0.5, 此時E最大,這時候說明信息對我們沒有明確的意義,對分類沒有幫助。

但是我們不僅僅想要變量的E最小,還想要這棵樹是 well organized。
所以用到 Gain:信息增益

意思是如果我后面要用這個變量的話,它的E會減少多少。

例如下面的數據集:

-
先計算四個feature的熵E,及其分支的熵,然后用Gain的公式計算信息增益。
-
再選擇Gain最大的特征是 outlook。
-
第一層選擇出來后,各個分支再繼續選擇下一層,計算Gain最大的,例如分支 sunny 的下一層節點是 humidity。
詳細的計算步驟可以參考這篇博文。
C4.5
ID3有個局限是對於有大量數據的feature過於敏感,C4.5是它的一個改進,通過選擇最大的信息增益率 gain ratio 來選擇節點。而且它可以處理連續的和有缺失值的數據。

P’ (j/p) is the proportion of elements present at the position p, taking the value of j-th test.
例如 outlook 作為第一層節點后,它有 3 個分支,分別有 5,4,5 條數據,則 SplitInfo(5,4,5) = -5/14log(5,14)-4/14log(4,14)-5/14(5,14) ,其中 log(5,14) 即為 log2(5/14)。
下面是一個有連續值和缺失值的例子:

連續值
第一步計算 Gain,除了連續值的 humudity,其他步驟和前文一樣。
要計算 humudity 的 Gain 的話,先把所有值升序排列:
{65, 70, 70, 70, 75, 78, 80, 80, 80, 85, 90, 90, 95, 96}
然后把重復的去掉:
{65, 70, 75, 78, 80, 85, 90, 95, 96}
如下圖所示,按區間計算 Gain,然后選擇最大的 Gain (S, Humidity) = 0.102

因為 Gain(S, Outlook) = 0 .246,所以root還是outlook:

缺失值
處理有缺失值的數據時候,用下圖的公式:

例如 D12 是不知道的。
-
計算全集和 outlook 的 info,
-
其中幾個分支的熵如下,再計算出 outlook 的 Gain:
比較一下 ID3 和 C4.5 的准確率和時間:
accuracy :

execution time:

4. 編碼實現算法?
代碼可以看《機器學習實戰》這本書和這篇博客。
完整代碼可以在 github 上查看。
接下來以 C4.5 的代碼為例:
1. 定義數據:
1 def createDataSet(): 2 dataSet = [[0, 0, 0, 0, 'N'], 3 [0, 0, 0, 1, 'N'], 4 [1, 0, 0, 0, 'Y'], 5 [2, 1, 0, 0, 'Y'], 6 [2, 2, 1, 0, 'Y'], 7 [2, 2, 1, 1, 'N'], 8 [1, 2, 1, 1, 'Y']] 9 labels = ['outlook', 'temperature', 'humidity', 'windy'] 10 return dataSet, labels
2. 計算熵:
1 def calcShannonEnt(dataSet): 2 numEntries = len(dataSet) 3 labelCounts = {} 4 for featVec in dataSet: 5 currentLabel = featVec[-1] 6 if currentLabel not in labelCounts.keys(): 7 labelCounts[currentLabel] = 0 8 labelCounts[currentLabel] += 1 # 數每一類各多少個, {'Y': 4, 'N': 3} 9 shannonEnt = 0.0 10 for key in labelCounts: 11 prob = float(labelCounts[key])/numEntries 12 shannonEnt -= prob * log(prob, 2) 13 return shannonEnt
3. 選擇最大的gain ratio對應的feature:
1 def chooseBestFeatureToSplit(dataSet): 2 numFeatures = len(dataSet[0]) - 1 #feature個數 3 baseEntropy = calcShannonEnt(dataSet) #整個dataset的熵 4 bestInfoGainRatio = 0.0 5 bestFeature = -1 6 for i in range(numFeatures): 7 featList = [example[i] for example in dataSet] #每個feature的list 8 uniqueVals = set(featList) #每個list的唯一值集合 9 newEntropy = 0.0 10 splitInfo = 0.0 11 for value in uniqueVals: 12 subDataSet = splitDataSet(dataSet, i, value) #每個唯一值對應的剩余feature的組成子集 13 prob = len(subDataSet)/float(len(dataSet)) 14 newEntropy += prob * calcShannonEnt(subDataSet) 15 splitInfo += -prob * log(prob, 2) 16 infoGain = baseEntropy - newEntropy #這個feature的infoGain 17 if (splitInfo == 0): # fix the overflow bug 18 continue 19 infoGainRatio = infoGain / splitInfo #這個feature的infoGainRatio 20 if (infoGainRatio > bestInfoGainRatio): #選擇最大的gain ratio 21 bestInfoGainRatio = infoGainRatio 22 bestFeature = i #選擇最大的gain ratio對應的feature 23 return bestFeature
4. 划分數據,為下一層計算准備:
1 def splitDataSet(dataSet, axis, value): 2 retDataSet = [] 3 for featVec in dataSet: 4 if featVec[axis] == value: #只看當第i列的值=value時的item 5 reduceFeatVec = featVec[:axis] #featVec的第i列給除去 6 reduceFeatVec.extend(featVec[axis+1:]) 7 retDataSet.append(reduceFeatVec) 8 return retDataSet
5. 多重字典構建樹:
1 def createTree(dataSet, labels): 2 classList = [example[-1] for example in dataSet] # ['N', 'N', 'Y', 'Y', 'Y', 'N', 'Y'] 3 if classList.count(classList[0]) == len(classList): 4 # classList所有元素都相等,即類別完全相同,停止划分 5 return classList[0] #splitDataSet(dataSet, 0, 0)此時全是N,返回N 6 if len(dataSet[0]) == 1: #[0, 0, 0, 0, 'N'] 7 # 遍歷完所有特征時返回出現次數最多的 8 return majorityCnt(classList) 9 bestFeat = chooseBestFeatureToSplit(dataSet) #0-> 2 10 # 選擇最大的gain ratio對應的feature 11 bestFeatLabel = labels[bestFeat] #outlook -> windy 12 myTree = {bestFeatLabel:{}} 13 #多重字典構建樹{'outlook': {0: 'N' 14 del(labels[bestFeat]) #['temperature', 'humidity', 'windy'] -> ['temperature', 'humidity'] 15 featValues = [example[bestFeat] for example in dataSet] #[0, 0, 1, 2, 2, 2, 1] 16 uniqueVals = set(featValues) 17 for value in uniqueVals: 18 subLabels = labels[:] #['temperature', 'humidity', 'windy'] 19 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) 20 # 划分數據,為下一層計算准備 21 return myTree
6. 可視化決策樹的結果:
dataSet, labels = createDataSet() labels_tmp = labels[:] desicionTree = createTree(dataSet, labels_tmp) treePlotter.createPlot(desicionTree)
