決策樹算法的理解及實現
本文基本復制原文來源:http://www.cnblogs.com/lliuye/p/9008901.html,我個人認為已經非常詳細了,所有理論基本來自周志華《機器學習》的決策樹章節!
我主要是將該博客提供的源碼進行了實踐與大量注解,以便讀者更容易理解。而為了讀者方便理解,我將注解提供在源碼內。經過源碼注解,我已將作者小錯誤
classCount+=1改成classCount[value]+=1。我將代碼附在原理后面,你可以通過debug形式解讀代碼等,因為代碼已經是可以直接運行的,希望對學習機器學習的
決策樹算法有幫助。
1. 決策樹
決策樹(decision tree)是一種基本的分類與回歸方法(本文主要是描述分類方法),是基於樹結構進行決策的,可以將其認為是if-then規則的集合。一般的,一棵決策樹包含一個根節點、若干內部節點和若干葉節點。其中根節點包含所有樣本點,內部節點作為划分節點(屬性測試),葉節點對應於決策結果。
用決策樹進行分類,是從根節點開始,對實例的某一特征進行測試,根據測試結果,將實例分配到其子節點,若該子節點仍為划分節點,則繼續進行判斷與分配,直至將實例分到葉節點的類中。
若對以上描述不太明白,可以結合以下圖進行理解。

根據以上決策樹,現在給你一個實例:{色澤:青綠,根蒂:稍蜷,敲聲:清脆,紋理:清晰,臍部:稍凹,觸感:光滑},來判斷該瓜是否是好瓜。其過程是:臍部(稍凹)-->根蒂(稍蜷)-->色澤(青綠)-->好瓜。
以上是由決策樹來進行分類的過程。而決策樹的學習(構建)通常是一個遞歸地選擇最優特征的過程。那么構建決策樹時如何選擇特征作為划分點(即選擇哪個特征作為根節點或者選擇哪個特征作為非葉子節點)?當訓練數據量大、特征數量較多時構建的決策樹可能很龐大,這樣的決策樹用來分類是否好?
由這些問題我們可以知道,構建決策樹的三個要點:
(1)特征選擇
(2)決策樹的生成
(3)決策樹修剪
2. ID3算法
基於ID3算法的決策樹構建,其選擇特征的准則是信息增益。信息增益(information gain)表示得知特征 XX的信息而使得類 YY 的信息的不確定性減少的程度。也就是說,信息增益越大,通過特征 XX ,就越能夠准確地將樣本進行分類;信息增益越小,越無法准確進行分類。
在介紹信息增益之前,我們需要先對熵進行一下講解。
2.1 熵(Entropy)
熵是度量樣本集合純度最常用的一種指標,它是信息的期望值。我們首先了解一下什么是信息。由《機器學習實戰》中定義:
如果待分類的事務可能划分在多個分類之中,則符號(特征) kk 的信息定義為:
l(k)=−log2p(k)l(k)=−log2p(k)
其中 p(k)p(k) 為選擇該分類的概率。
而熵計算的是所有類別所有可能值包含的信息期望值,其公式為:
其中 NN 為類別個數。
現在我們使用例子,來理解熵的計算:

(1)對於最終分類(是否為好瓜),計算其信息熵:
由上表可看出,一共有17個樣本,屬於好瓜的有8個樣本,壞瓜的有9個樣本,因此其熵為:
(2)對於特征“色澤”,計算其信息熵:
由於特征“色澤”取值有:{青綠,烏黑,淺白}。若使用該屬性對 DD 進行划分,可得到3個子集,分別記為: D1D1 (色澤=青綠), D2D2 (色澤=烏黑), D3D3 (色澤=淺白)。
其中 D1D1 包含樣本 1,4,6,10,13,171,4,6,10,13,17 ,其中類別為好瓜的比例為 p1=36p1=36 ,壞瓜的比例為 p2=36p2=36 ; D2D2 包含樣本 2,3,7,8,9,152,3,7,8,9,15 ,其中類別為好瓜的比例 p1=46p1=46 ,壞瓜的比例為 p2=26p2=26 ; D3D3 包含樣本 5,11,12,14,165,11,12,14,16 ,其中類別為好瓜的比例 p1=15p1=15 ,壞瓜的比例為 p2=45p2=45 ,因此其三個分支點的信息熵為:
2.2 信息增益(information gain)
信息增益,由《統計學習方法》中定義:
特征 aa 對訓練數據集 DD 的信息增益 Gain(D,a)Gain(D,a) ,定義為集合 DD 的經驗熵(即為熵)與特征 aa 給定條件下的經驗條件熵 Ent(D|a)Ent(D|a) 之差,即:
Gain(D,a)=Ent(D)−Ent(D|a)Gain(D,a)=Ent(D)−Ent(D|a)
其中特征 aa 將數據集划分為: D1,D2,...,DvD1,D2,...,Dv,而經驗條件熵為:Ent(D|a)=∑i=1v|Di||D|Ent(Di)Ent(D|a)=∑i=1v|Di||D|Ent(Di)
我們根據例子對其進行理解:
對於特征“色澤”,我們計算其信息增益,由2.1中,集合 DD 的熵為: Ent(D)=0.998Ent(D)=0.998 ,對於特征“色澤”的三個分支點的熵為: Ent(D1)=1.000,Ent(D2)=0.918,Ent(D3)=0.722Ent(D1)=1.000,Ent(D2)=0.918,Ent(D3)=0.722,則“色澤”特征的信息增益為:
2.3 算法步驟
ID3算法遞歸地構建決策樹,從根節點開始,對所有特征計算信息增益,選擇信息增益最大的特征作為節點的特征,由該特征的不同取值建立子節點;再對子節點遞歸地調用以上方法構建決策樹;知道所有特征的信息增益均很小或者沒有特征可以選擇為止。最后得到一個決策樹。
在算法中(C4.5也是),有三種情形導致遞歸返回:
(1)當前節點包含的樣本全屬於同一類別,無需划分。
(2)當前屬性集為空,或是所有樣本在所有屬性上取值相同,無法划分。(此時將所含樣本最多的類別設置為該葉子節點類別)
(3)當前節點包含的樣本集合為空,不能划分。(將其父節點中樣本最多的類別設置為該葉子節點的類別)
輸入:訓練數據集 DD ,特征集 AA , 閾值 ϵϵ ;
過程:函數 TreeGenerate(D,A)TreeGenerate(D,A) .
1:計算節點信息增益 Gain(D,a)Gain(D,a) :
2: 節點a的熵: Ent(D,a)Ent(D,a)
3: 節點D的熵: Ent(D)Ent(D)
4: 節點信息增益: Gain(D,a)=Ent(D)−Ent(D,a)Gain(D,a)=Ent(D)−Ent(D,a)
5:生成節點node:
6:if DD 中樣本全屬於同一類別 CC then
7: 將node標記為 CC 類葉節點;return
8:end if
9:if A=∅A=∅ OR DD 中樣本在 AA 上取值相同then
10: 將node標記為葉節點,期類別標記為 DD 中樣本數最多的類;return
11:end if
12:按照節點信息增益,從 AA 中選擇最優划分屬性 a∗a∗
13:for a∗a∗ 中的每一個值 ai∗a∗i do
14: 為node生成一個分支;令 DiDi 表示 DD 中在 a∗a∗ 上取值為 ai∗a∗i 的樣本子集;
15: if DiDi 為空,then
16: 將分支節點標記為葉節點,其類別標記為 DD 中樣本最多的類;return
17: else
18: 以 TreeGenerate(Di,A/a∗)TreeGenerate(Di,A/a∗) 為分支節點
19: end if
20:end for
輸出:以node為根節點的一棵決策樹
3. C4.5算法
實際上,信息增益准則對可取值書目較多的屬性有所偏好,例如如果將前面表格中的第一列ID也作為特征的話,它的信息增益將達到最大值,而這樣做顯然不對,會造成過擬合。為了減少這種偏好可能帶來的不利影響,C4.5算法中將采用信息增益比來進行特征的選擇。信息增益比准則對可取值數目較少的屬性有所偏好。接下來,我們首先對信息增益比進行介紹。
3.1 信息增益比(增益率)
信息增益比的定義為:
其中:
我們根據例子對其進行理解:
對於特征“色澤”,我們計算其信息增益比,由2.2計算得 Gain(D,色澤)=0.109Gain(D,色澤)=0.109,而
則 Gain_ratio(D,色澤)=0.1091.580=0.069Gain_ratio(D,色澤)=0.1091.580=0.069。
3.2 算法步驟
C4.5算法同ID3算法過程相似,僅在選擇特征時,使用信息增益比作為特征選擇准則。
輸入:訓練數據集 DD ,特征集 AA , 閾值 ϵϵ ;
過程:函數 TreeGenerate(D,A)TreeGenerate(D,A) .
1:計算節點信息增益比 Gainratio(D,a)Gainratio(D,a) :
2: 節點a的熵: Ent(D,a)Ent(D,a)
3: 節點D的熵: Ent(D)Ent(D)
4: 節點信息增益: Gain(D,a)=Ent(D)−Ent(D,a)Gain(D,a)=Ent(D)−Ent(D,a)
5: 節點固定值: IV(a)IV(a)
6: 節點信息增益比: Gainratio(D,a)=Gain(D,a)IV(a)Gainratio(D,a)=Gain(D,a)IV(a)
7:生成節點node:
8:if DD 中樣本全屬於同一類別 CC then
9: 將node標記為 CC 類葉節點;return
10:end if
11:if A=∅A=∅ OR DD 中樣本在 AA 上取值相同then
12: 將node標記為葉節點,期類別標記為 DD 中樣本數最多的類;return
13:end if
14:按照節點信息增益,從 AA 中選擇最優划分屬性 a∗a∗
15:for a∗a∗ 中的每一個值 ai∗a∗i do
16: 為node生成一個分支;令 DiDi 表示 DD 中在 a∗a∗ 上取值為 ai∗a∗i 的樣本子集;
17: if DiDi 為空,then
18: 將分支節點標記為葉節點,其類別標記為 DD 中樣本最多的類;return
19: else
20: 以 TreeGenerate(Di,A/a∗)TreeGenerate(Di,A/a∗) 為分支節點
21: end if
22:end for
輸出:以node為根節點的一棵決策樹
4. 剪枝處理
針對於在第1部分提到的最后一個問題:當訓練數據量大、特征數量較多時構建的決策樹可能很龐大,這樣的決策樹用來分類是否好?答案是否定的。決策樹是依據訓練集進行構建的,當決策樹過於龐大時,可能對訓練集依賴過多,也就是對訓練數據過度擬合。從訓練數據集上看,擬合效果很好,但對於測試數據集或者新的實例來說,並不一定能夠准確預測出其結果。因此,對於決策樹的構建還需要最后一步----即決策樹的修剪。
決策樹的修剪,也就是剪枝操作,主要分為兩種:
(1)預剪枝(Pre-Pruning)
(2)后剪枝(Post-Pruning)
接下來我們將詳細地介紹這兩種剪枝方法。
4.1 預剪枝(Pre-Pruning)
預剪枝是指在決策樹生成過程中,對每個節點在划分前先進行估計,若當前節點的划分不能帶來決策樹泛化性能的提升,則停止划分並將當前節點標記為葉節點。
我們使用例子進一步理解預剪枝的過程:
將本文開始的西瓜數據集表划分成兩部分,一部分作為訓練集用來構建決策樹,一部分作為驗證集用來進行決策樹的剪枝。具體划分見下圖:

使用ID3算法進行決策樹的構建,即使用信息增益進行特征的選擇。首先選擇特征“臍部”作為決策樹根節點,如何判斷該節點是否需要剪枝,需要對剪枝前后驗證集精度進行比較。由“臍部”這個特征將產生三個分支“凹陷”、“稍凹”、“平坦”,並認定其分支結果(可采用多數表決法,當分類數量相當時,任選一類即可),如下圖:

查看驗證集,若將“臍部”看做節點,並將其標記為“好瓜”,那么划分前的精度為: 37=0.42937=0.429。符合“臍部”=“凹陷”的樣本有: 4,5,134,5,13 ,其中正樣本(是好瓜)為 4,54,5 ,正樣本個數為2,按照上圖預測正確數為2;同理“臍部”=“稍凹”的樣本中正樣本個數為1,預測正確數為1;“臍部”=“平坦”的樣本中負樣本個數為2,預測正確個數為2。因此使用“臍部”這個特征進行划分,划分后的精度為: 57=0.71457=0.714。由於預測后精度大於預測前精度,因此不對“臍部”進行剪枝,即將其作為划分點進行划分。

同理我們“色澤”以及“根蒂”特征進行划分前后精度的計算。對於“色澤”,划分后的精度為 0.5710.571 ,而划分前為 0.7140.714 ,划分使得結果變差,因此不以該特征進行划分,即將該節點記為葉子節點並標記為“好瓜”;同理“根蒂”特征划分前后的精度都為 0.7140.714 ,效果並未提升,因此也不將該特征進行划分,而是將其作為葉子節點並標記為“好瓜”。由此,決策樹構建完畢。此時的決策樹為只有一層的樹。
可有由圖中看出,該決策樹有點過於簡單,雖然降低的過擬合的風險,但是由於其基於“貪心”的本質禁止了其它分支的展開,給預剪枝決策樹帶來了欠擬合的風險。
4.1 后剪枝(Post-Pruning)
后剪枝是指先從訓練集生成一棵完整的決策樹,然后自底向上地對非葉節點進行考察,若將該節點對應的子樹替換為葉節點能帶來決策能力的提升,則將該子樹替換成葉節點。
我們使用例子進一步理解后剪枝的過程:
同樣適用4.1中的划分數據集。針對已建立好的決策樹,我們首先對“紋理”特征節點進行處理,判斷其是否需要剪枝,見下圖。

首先,使用整個決策樹對驗證集進行預測,並將其預測結果與真實結果進行對比,可得到如下結果(預測結果與真實結果相同,標記為“正確”,否則標記為“不正確”):
首先我們判斷是否需要對“紋理”進行剪枝:剪枝前精確度由上結果可以得到為 37=0.42937=0.429 ,剪枝后(即將該節點標記為“好瓜”),此時對於樣本 ((8,正確))((8,正確)) ,其它樣本結果不變,其精度提升到 47=0.57147=0.571 ,因此對該節點進行剪枝。對節點5“色澤”,剪枝前精確度為 0.5710.571 ,剪枝后仍舊為 0.5710.571 ,對此我們可以不進行剪枝(但在實際情況下仍舊會需要剪枝);同理對“根蒂”、節點2“色澤”進行計算,所得結果見上圖。由此得到后剪枝決策樹。
后剪枝決策樹通常比預剪枝決策樹保留了更多的分支,一般情況下,后剪枝決策樹欠擬合的風險很小,其泛化能力往往優於預剪枝預測數。但由於其是基於創建完決策樹之后,再對決策樹進行自底向上地剪枝判斷,因此訓練時間開銷會比預剪枝或者不剪枝決策樹要大。
代碼如下:
from math import log
import operator
# C4.5算法與ID3算法僅有細微差別,其差別與代碼在注釋中體現
# #-------------------------------------構造決策樹-----------------------------------------
# 計算給定數據集的香農熵
def calcShannonEnt(dataSet):
'''
:param dataSet: 樣本第一維度是樣本個數,最后一個維度是每個樣本的類別
:return: 計算出每個類別的香濃熵
'''
numEntries = len(dataSet) # 計算樣本個數
labelCounts = {} # 用來統計每個標簽的個數,如有3類,0類4個,1類5個,3類32個,則{0:4,1:5,2:32}
for featVec in dataSet: # 遍歷樣本
currentLabel = featVec[-1] # currentLabel保存分類標簽 數據集最后一個為類別
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannanEnt = 0.0
# shannonEnt = -(求和)[p(xi)log(2,p(xi))]
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
# 以2為底求對數
shannanEnt -= prob * log(prob, 2)
return shannanEnt
# 按照給定特征划分數據集
def splitDataSet(dataSet, axis, value):
'''
:param dataSet: 數據集
:param axis: 想要刪掉的列
:param value: 判斷axis列刪掉的值是否等於value,否則不能刪除。原因在於該
chooseBestFeatureToSplit函數將每個列循環,而value將某列值循環,這樣就可以排除
axis列的值不再value中而刪除了。
:return: 返回一個axis列滿足value值的新數據,如[array([21., 1., 61.]), array([ 17., 41., 1.]),
'''
# 將dataSet中滿足dataSet[axis] = value的行進行保留,
# retDataSet中存儲了保留行中除了axis列之外的其它列
retDataSet = []
for featVec in dataSet:
# 除掉featVec[axis]列的內容
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
# extend() 函數用於在列表末尾一次性追加另一個序列中的多個值(用新列表擴展原來的列表)
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
# 遍歷數據集,循環計算香農熵和splitDataSet()函數,
# 找到最好的划分方式(計算所有特征的信息增益,並進行比較選出最優的特征)
def chooseBestFeatureToSplit(dataSet):
'''
該函數執行一次便可以選擇一個最好的節點
:param dataSet: 需要篩選節點時候的所有數據集和,
行表示樣本數,列最后一列表示最終分類,其它列為屬性值,
每個屬性可能包含多個特征
:return: 返回最好的節點序號,如第3列屬性最好,便返回3.(0列屬性也包含)
'''
# 計算總特征數,數據集最后一列為分類標簽。dataSet[0]是指第一條數據
numFeatures = len(dataSet[0]) - 1 # 排除最后一列的干擾
# 按照分類標簽計算香農熵
# baseEntropy為經驗熵H(D)
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeaature = -1
# 創建唯一的feature取值列表(分別對每個feature進行),對每個唯一feature值划分一次數據集
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # 得到dataSet數據集中第i列所有值,集第i個屬性
uniqueVals = set(featList) # 第i個屬性不同特征的集合,排除相同的特征
newEntropy = 0.0 # newEntropy為經驗條件熵H(D|A)
# 計算每種划分方式的信息熵,並求該feature熵和
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
# subDataSet是feature[i]=value的所有條目的列表(不包含feature[i])
# len(subDataSet)表示feature[i]=value的條目總數
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
# (C4.5)分裂信息:splitInfo
# splitInfo -= prob * log(prob, 2)
# 信息增益:g(D,A) = H(D) - H(D|A)
inforGain = baseEntropy - newEntropy
# (C4.5)信息增益率
# inforGainRate = inforGain / splitInfo
if inforGain > bestInfoGain:
bestInfoGain = inforGain
bestFeaature = i
return bestFeaature
def majorityCnt(classList):
'''
該函數是構建決策樹停止條件之一,當數據集只剩下最后一列的標簽時候,
會出現多個標簽,則“投票法”,將選擇標簽最多的一個為最終標簽
:param classList: 為最終標簽類的一列
:return: 返回一個最終標簽
'''
classCount = {} # 保存數據集classList每個類別的數量
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) # 默認為從小到大
'''
a = [1,2,3]
>>> b=operator.itemgetter(1) //定義函數b,獲取對象的第1個域的值
>>> b(a)
2
>>> b=operator.itemgetter(1,0) //定義函數b,獲取對象的第1個域和第0個的值
>>> b(a)
(2, 1)
'''
return sortedClassCount[0][0] # 返回數量最多類的名字
# 創建樹的函數代碼
def createTree(dataSet, labels):
'''
:param dataSet: (m,n)表示m行n列,第0--(n-1)列表示屬性,
第n列表示每個樣本的分類結果,屬於哪一類。
:param labels: 是屬性對應的標簽,如有3個屬性分別為第0、1、2列,
那標簽應為['顏色','紋理','觸感']
:return:函數返回的是myTree,則在myTree[bestFeatLabel][value]建立
myTree = {bestFeatLabel: {}},一直在myTree[bestFeatLabel][value]建立
'''
classList = [example[-1] for example in dataSet] # 創建樣本類別標簽列表
# 下面的代碼用最終分類即標簽判斷是否停止循環
if classList.count(classList[0]) == len(classList): # 類別完全相同則停止繼續划分
# classList.count(classList[0])表示該列classList中第一名字的數量
return classList[0] # 返回最終的結果
# 遍歷完所有特征時,返回出現次數最多的類別(dataset中只剩下一列類別)
if len(dataSet[0]) == 1:
# 表示只剩下類別了,這里處理最后一個節點應該給最終標簽是什么。
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) # 得到dataSet數據集中最好的屬性作為節點,為數字型式
bestFeatLabel = labels[bestFeat] # 該屬性作為節點的名字
myTree = {bestFeatLabel: {}} # 建立最好的屬性值為空
del(labels[bestFeat]) # 刪除該節點
# 得到列表包含的所有屬性值
featValues = [example[bestFeat] for example in dataSet] # 得到bestFeat列所有值
uniqueVals = set(featValues) # 得到bestFeat列的屬性值
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] =\
createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
# 已經是新的數據集了 新的標簽
return myTree # 返回值是myTree
# 簡單測試數據集
def createDataSet():
dataSet = [[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
#-------------------------------------構建分類器-----------------------------------------
def classify(inputTree, featLabels, testVec):
'''
:param inputTree: 輸入的是樹的結構,以字典形式給出
:param featLabels: 輸入測試數據的屬性,要有訓練數據的屬性
:param testVec: 測試數據,直接為1列,只有屬性沒有最終的類
:return: 返回了最終的分類屬性
'''
firstStr = list(inputTree.keys())[0] # 樹的第一個節點的名字
restDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr) # 得到樹firstStr節點對應的屬性
'''
若樹為{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
firstStr就是得到樹的第一個節點的名字no surfacing
restDict就是得到'no surfacing'的其它字典,形式為
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
featIndex 在labels找到對應的標簽
'''
for key in restDict.keys():
if testVec[featIndex] == key:
# testVec 測試數據集在featIndex下只有一個值
if type(restDict[key]).__name__ == 'dict':
classLabel = classify(restDict[key], featLabels, testVec)
else:
classLabel = restDict[key] # 因為這里只剩下該key鍵值的一個最終類
return classLabel # 返回了最終的分類屬性
#-------------------------------------存儲決策樹-----------------------------------------
def storeTree(inputTree, filename):
'''
:param inputTree: 輸入樹的字典
:param filename: 需要保存的文件路徑
'''
import pickle
# fw = open(filename, "w")
fw = open(filename, "wb+")
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
'''
:param filename: 從文件中讀取字典
:return:
'''
import pickle
fr = open(filename, "rb+")
return pickle.load(fr)
# 代碼實現的主函數
if __name__ == '__main__':
# 構建訓練數據集
dataSet, labels = createDataSet()
# 訓練模型生成決策樹
decision_tree_dict = createTree(dataSet, labels)
print(decision_tree_dict) # 打印決策樹,為字典類型保存
# 構建測試數據集
featLabels=['no surfacing','flippers']
testVec=[0,1]
# 采用下面的分類函數,就可以對測試數據進行分類了
test_result=classify(decision_tree_dict, featLabels, testVec)
print(test_result) # 打印最終測試結果
結果如下:
最后:本人僅僅對代碼進行了改動與詳細的注解,感謝下方博客作者的提供,若有所見解或異議可在下方評論,謝謝!
原文來源:http://www.cnblogs.com/lliuye/p/9008901.html