前言
生活中有很多利用決策樹的例子。西瓜書上給的例子是西瓜問題(講到這突然想到書中不少西瓜的例子,難道這就是它西瓜封面的由來?)\。大致意思是,已經有一堆已知好瓜壞瓜的西瓜,每次挑取西瓜的一條屬性,將西瓜進行分類。然后在分類的西瓜中,繼續挑取下一條屬性進行更加細致的划分,直到所有的屬性被用完。
這個例子有一個隱含的前提,就是給出所有的屬性,它唯一地決定西瓜的好壞。意思是,不存在兩個都是好瓜的瓜,或者都是壞瓜的瓜,它們的所有屬性都相同。這有點類似數據庫中的實體完整性。這樣,你才能通過構建的決策樹,按照它的用來分類的屬性的順序,每次前進一個分支,直到最后一層就能知道待測西瓜是否是好瓜。
但是如果由許多瓜,有好瓜也有壞瓜,他們的屬性都相同的時候,就意味着你沿決策樹到達葉子節點的時候,面臨多種選擇。這時候,可以選擇出現次數最多的瓜種作為結果返回。
(這是用手機從西瓜書上拍下來的。結果上傳的時候才發現忘記設置照片大小了,2M)

決策樹的最關鍵的問題是,如何選擇划分屬性的順序才能使得決策樹的平均性能最好(即平均在樹上走最少的路徑就可以知道結果)。比如說,如果觸感就能夠唯一決定一個瓜是否是好壞,但是觸感確實放在最后進行划分的,那么前面走的所有步驟都是多余的。如果一開始就用觸感來進行划分,那么只需要走一步,就能夠知道西瓜是否是好瓜。
下面來解釋一個概念,它能夠決定用何種屬性進行划分。
熵
划分數據集最大的原則就是:將無序的數據變得更加有序。我們選取的是,能夠將數據划分得“最有序”的屬性。換句話說,選取能夠給出最多信息的屬性進行划分。打個比方,我要判斷一個蚊子是否是母蚊子,你首先告訴我它有翅膀。可是所有的蚊子都有翅膀,這對我進行分類毫無意義,就稱“有無翅膀”這一屬性完全沒有給出任何信息。如果你告訴我它經常在你身邊上下騰飛騷擾你,那么我能夠給出八成的可能說它是母蚊子,就說它給出了部分信息。你如果告訴我它會吸血,那母蚊子沒跑了。“會不會吸血”這一屬性給出了決定性的信息。
熵(Entropy)定義為信息的期望值,它在物理學上表示物質的混亂程度。在信息學上可以作類比。熵增加,表示信息更加混亂,熵減,表示信息更加有序。假設划分之前,這堆西瓜的熵是$Ent(D)$,按照某種屬性划分之后這堆西瓜的熵是$Ent(D')$,注意后者一定小於等於前者。否則划分就沒有任何意義。這兩者的差,就是這個屬性對於這堆西瓜有序化的貢獻。下面說明熵的計算方法:
假設樣本集$D$中,第$k$類樣本所占的比例為$p_k$,那么$D$的熵:
$Ent(D) = - \Sigma p_klog_2p_k$
這里約定$p_k = 0$時,$p_klog_2p_k = 0$。當這樣本集被某個屬性划分成$v$份之后,熵就是所有子集的熵的和:
$Ent(D') = -\Sigma Ent(D^i) \ \ ,i = [1,...,v]$
但是,每個樣本子集中划分到的樣本數量不一樣,需要對每個子集的熵進行加權:
$Ent(D') = -\Sigma \frac{|D^i|}{|D|}Ent(D^i) \ \ ,i = [1,...,v]$
於是熵增就定義為
$Gain(D,a) = Ent(D) - Ent(D') = Ent(D) - \Sigma \frac{|D^i|}{|D|}Ent(D^i) \ \ ,i = [1,...,v]$
表示,按照屬性a對D進行划分熵的增益。顯然,增益越大,表示越應該提前使用該屬性進行划分。
下面給出計算熵增的python2.x 代碼
def calcShannonEnt(dataSet): numEntries = len(dataSet) labelCounts = {} for featVec in dataSet: currentLabel = featVec[-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
這里的dataSet,由多個向量組成,每個向量代表一個樣本。這里的向量的最后一個元素代表該樣本的類別。這不同於之前的KNN算法。KNN的類別和屬性是分開表示的。
第一個for循環計算每個類別的概率。第二個for循環計算熵。
決策樹完整算法
輸入:樣本集D,每個樣本包含他們的屬性(值)和類別。
輸出:決策樹
算法描述:
- 選擇一個最好的划分屬性
- 用最好的屬性划分D,並從屬性列表中刪除該屬性。
- 對每個子集,如果屬性已經用完,返回類別標簽。否則對每個子集,重復1步驟。
python2.x 代碼實現
# 使用坐標為axis的屬性划分dataSet,將該屬性的值等於value的樣本返回。並且從樣本中刪除該屬性 def splitDataSet(dataSet,axis,value): retDataSet = [] for featVec in dataSet: if featVec[axis] == value: reducedFeatVec = featVec[:axis] reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet # 選擇最好的划分屬性,它返回的是屬性在屬性向量中的位置(坐標) def chooseBestFeatureToSplit(dataSet): 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 # dataSet 中的向量包含了屬性和類別 def createTree(dataSet,labels): # 取所有的類別 classList = [example[-1] for example in dataSet] # 如果所有的類別都相同,就返回 if classList.count(classList[0]) == len(classList): return classList[0] # 如果dataSet中已經沒有屬性了,表示所有的屬性已經被遍歷完,就返回出現次數最多的屬性 if len(dataSet[0] == 1): return majorityCnt(classList) # 選擇一個最好的划分屬性 bestFeat = chooseBestFeatureToSplit(dataSet) bestFeatLabel = labels[bestFeat] 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 majorityCnt(classList): classCount={} for vote in classList: if vote not in classCount.keys():classCount[vote] = 0 classCount[vote]+=1 sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1),reverse = True) return sortedClassCount[0][0]
這里的決策樹用的是python的字典嵌套來表示。字典每一層的key由屬性名和屬性值交替,如果這一層的key是屬性名,表示由該屬性名進行划分,下一層的字典就是該屬性的所有可能的取值。然后每一個對於每一個取值,再選一個屬性名。如果沒有屬性名可選,最終的字典就是類別。具體的含義可以參考這篇博客:python中字典{}的嵌套。他里面用的例子剛好是這篇機器學習的代碼。
最后一個函數是為了解決葉子節點有多種結果情況。選擇出現次數最多的結果返回。函數createTree中第二個參數labels是屬性的名稱,一般類型是字符串,比如‘色澤’,‘外觀’,‘觸感’之類的。而第一個參數dataSet中的屬性是屬性的值。
其他
上述代碼中構造的決策樹是一個嵌套字典。具體如何使用,其實就是下標索引,在上面給出的博客中也有講。
決策樹的構建過程耗時比較長,但是構建好之后測試樣本就很快。他不同於上一篇的KNN,KNN每次需要測試樣本的時候,都需要重新訓練一次,即‘懶惰學習’。決策樹只有在拿到新的訓練樣本的時候才需要訓練,以后需要測試樣本都可以調用已經構建好的決策樹,即‘急切學習’。關於構建出來的用嵌套字典表示的決策樹如何繪制出來和存儲,在《機器學習實戰》里面也有詳細提到,分別用matplotlib和pickle庫。因為我這兩個都不會用,python也是先學不到兩周,還需要學習,所以就不搬運了。如果什么時候覺得這非常需要作篇博客以表學習,那到時就另起一篇,放在python分類中。
增益率
西瓜書還提到這個概念。
是想,如果把訓練樣本的每一個樣本都作編號,然后把編號也作為屬性參與決策。通過計算,編號產生的信息熵增益達到0.998,遠高於其他屬性。這很好理解:每一個編號都產生一個分支,每個分支僅僅包含一個樣本,這樣就完全不需要考慮其他屬性了。然而,這樣的決策樹並不具有泛化能力,無法對新樣本做出有效預測。
信息增益准則對取值數目多的屬性有偏好。意思是,一個屬性的取值越多,越有可能被選為當前最好的划分屬性。為了解決這種偏好帶來的不利映像,著名的C4.5決策樹算法不直接使用信息增益,而是使用“增益率”來選擇最優划分屬性。增益率定義為:
$Gain\_ratio(D,a) = \frac{Gain(D,a)}{IV(a)}$
其中
$IV(a) = -\Sigma \frac{|D^v|}{|D|}log_2\frac{|D^v|}{|D|}$
$IV(a)$稱為屬性a的“固有值”。屬性a的可能取值越多,固有值越大,增益率越小。可以看到,原本的信息增益使用屬性的固有值進行了加權,消除了原本算法對取值數目多的屬性的偏好帶來的影響。
西瓜書上還有一段:
需要注意的是,增益率准則對可取值數目較少的屬性有所偏好,因此,C4.5算法並不是直接選擇增益率最大的候選划分屬性,而是使用了一個啟發式:先從候選划分屬性中找出信息增益高於平均水平的屬性,再從中選擇增益率最高的。
