下表為是否適合打壘球的決策表,預測E= {天氣=晴,溫度=適中,濕度=正常,風速=弱} 的場合,是否合適中打壘球。
天氣 |
溫度 |
濕度 |
風速 |
活動 |
晴 |
炎熱 |
高 |
弱 |
取消 |
晴 |
炎熱 |
高 |
強 |
取消 |
陰 |
炎熱 |
高 |
弱 |
進行 |
雨 |
適中 |
高 |
弱 |
進行 |
雨 |
寒冷 |
正常 |
弱 |
進行 |
雨 |
寒冷 |
正常 |
強 |
取消 |
陰 |
寒冷 |
正常 |
強 |
進行 |
晴 |
適中 |
高 |
弱 |
取消 |
晴 |
寒冷 |
正常 |
弱 |
進行 |
雨 |
適中 |
正常 |
弱 |
進行 |
晴 |
適中 |
正常 |
強 |
進行 |
陰 |
適中 |
高 |
強 |
進行 |
陰 |
炎熱 |
正常 |
弱 |
進行 |
雨 |
適中 |
高 |
強 |
取消 |
如何發現這些數據之中所掩藏的規律,從而較好的預測在給定條件下,所可能的結果。決策樹是一種以示例為基礎的歸納學習方法,能夠較好的解決這類問題。
- 一個簡單的例子
請給出布爾函數(A * -B)+ C(+:或,*:與,-非)的最小體積(或結點)決策樹。
當C為1時,AB不管取何值整個表達式都為真,此時這個表達式就可以確定真假,所以選擇C作為頭結點。若C為0,表達式無法確定真假,還需進一步看AB的取值,A與非B是與的關系,兩者具有相同的地位,所以接下來無論取A還是B都可以,整個決策樹構造結果如下圖所示。
類似於這個簡單例子對於打壘球這些數據,我們可以將天氣,溫度,濕度,風速(可以成為屬性或特征)類比成布爾函數的ABC,而它們的取值,如天氣的取值可以是晴,雨,陰類比成ABC布爾取值真假,那么活動的取消或進行,就可以類比成整個布爾表達式的真或假。要構造一顆最小體積決策樹,就要每次在各個屬性中找到區分度最大的屬性來作為當前決策樹的節點。
- 相關名詞
熵
通常熵表示事物的混亂程度,熵越大表示混亂程度越大,越小表示混亂程度越小。對於隨機事件S,如果我們知道它有N種取值情況,每種情況發生的概論為,那么這件事的熵就定義為:
例如對於打壘球的例子,要求活動的熵H(活動)。在活動一欄屬性中發現活動的取值有兩種:取消(5個)和進行(9個),它們所占的比例分別為5/14,9/14。那么H(活動)的取值為:,算出的結果約為0.94。
對於熵的理解
如果一件事發生的可能是1,不發生的肯為0那么這件事的熵為=0,這就表明這件事肯定發生,沒有不發生的情況,那么它的混亂程度是最小的0。同理當不發生的可能是1,混亂程度也是0。當發生與不發生各占一半時,這件事就越不好確定,所以此時熵為最大,其圖像如下圖所示。
計算熵的代碼如下

1 def calcShannonEnt(dataSet):#計算香農熵 2 numEntries = len(dataSet) 3 4 labelCounts = {} 5 for featVec in dataSet: 6 currentLabel = featVec[-1] #取得最后一列數據,計算該屬性取值情況有多少個 7 if currentLabel not in labelCounts.keys(): 8 labelCounts[currentLabel] = 0 9 labelCounts[currentLabel]+=1 10 11 #計算熵 12 shannonEnt = 0.0 13 for key in labelCounts: 14 prob = float(labelCounts[key])/numEntries 15 shannonEnt -= prob*log(prob,2) 16 17 return shannonEnt
信息增益
隨機事件未按照某個屬划的不同取值划分時的熵減去按照某個屬性的不同取值划分時的平均熵。即前后兩次熵的差值。
還是對於打壘球的例子,未按照某個屬划的不同取值划分時的熵即H(活動)已算出未0.94。現在按照天氣屬性的不同取值來划分,發現天氣屬性有3個不同取值分別為晴,陰,雨。划分好后如下圖所示。
天氣 |
溫度 |
濕度 |
風速 |
活動 |
晴 |
炎熱 |
高 |
弱 |
取消 |
晴 |
炎熱 |
高 |
強 |
取消 |
晴 |
適中 |
高 |
弱 |
取消 |
晴 |
寒冷 |
正常 |
弱 |
進行 |
晴 |
適中 |
正常 |
強 |
進行 |
陰 |
炎熱 |
高 |
弱 |
進行 |
陰 |
寒冷 |
正常 |
強 |
進行 |
陰 |
適中 |
高 |
強 |
進行 |
陰 |
炎熱 |
正常 |
弱 |
進行 |
雨 |
寒冷 |
正常 |
強 |
取消 |
雨 |
適中 |
高 |
強 |
取消 |
雨 |
適中 |
高 |
弱 |
進行 |
雨 |
寒冷 |
正常 |
弱 |
進行 |
雨 |
適中 |
正常 |
弱 |
進行 |
在天氣為晴時有5種情況,發現活動取消有3種,進行有2種,計算現在的條件熵
=0.971
同理天氣為陰時有4種情況,活動進行的有4種,則條件熵為:
=0
同理天氣為雨時有5種情況,活動取消的有2種,進行的有3種,則條件熵為:
=0.971
由於按照天氣屬性不同取值划分時,天氣為晴占整個情況的5/14,天氣為陰占整個情況的4/14,天氣為雨占整個情況的5/14,則按照天氣屬性不同取值划分時的帶權平均值熵為:算出的結果約為0.693.
則此時的信息增益Gain(活動,天氣)= H(活動) - H(活動|天氣) = 0.94- 0.693 = 0.246
同理我們可以計算出按照溫度屬性不同取值划分后的信息增益:
Gain(活動,溫度)= H(活動) - H(活動|溫度) = 0.94- 0.911 = 0.029
按照濕度屬性不同取值划分后的信息增益:
Gain(活動,濕度)= H(活動) - H(活動|濕度) = 0.94- 0.789 = 0.151
按照風速屬性不同取值划分后的信息增益:
Gain(活動,風速)= H(活動) - H(活動|風速) = 0.94- 0.892 = 0.048
對於信息增益的理解
信息增益就是兩個熵的差,當差值越大說明按照此划分對於事件的混亂程度減少越有幫助。
計算各個屬性的信息增益,並選擇信息增益最大的屬性的代碼如下

1 #定義按照某個特征進行划分的函數splitDataSet 2 #輸入三個變量(待划分的數據集,特征,分類值) 3 #axis特征值中0代表no surfacing,1代表flippers 4 #value分類值中0代表否,1代表是 5 def splitDataSet(dataSet,axis,value): 6 retDataSet = [] 7 for featVec in dataSet:#取大列表中的每個小列表 8 if featVec[axis]==value: 9 reduceFeatVec=featVec[:axis] 10 reduceFeatVec.extend(featVec[axis+1:]) 11 retDataSet.append(reduceFeatVec) 12 13 return retDataSet #返回不含划分特征的子集 14 15 def chooseBestFeatureToSplit(dataSet): 16 numFeature = len(dataSet[0]) - 1 17 baseEntropy = calcShannonEnt(dataSet) 18 bestInforGain = 0 19 bestFeature = -1 20 21 for i in range(numFeature): 22 featList = [number[i] for number in dataSet]#得到某個特征下所有值(某列) 23 uniquelVals = set(featList) #set無重復的屬性特征值,得到所有無重復的屬性取值 24 25 #計算每個屬性i的概論熵 26 newEntropy = 0 27 for value in uniquelVals: 28 subDataSet = splitDataSet(dataSet,i,value)#得到i屬性下取i屬性為value時的集合 29 prob = len(subDataSet)/float(len(dataSet))#每個屬性取值為value時所占比重 30 newEntropy+= prob*calcShannonEnt(subDataSet) 31 inforGain = baseEntropy - newEntropy #當前屬性i的信息增益 32 33 if inforGain>bestInforGain: 34 bestInforGain = inforGain 35 bestFeature = i 36 37 return bestFeature#返回最大信息增益屬性下標
- 構造決策樹
決策樹的構造就是要選擇當前信息增益最大的屬性來作為當前決策樹的節點。因此我們選擇天氣屬性來做為決策樹根節點,這時天氣屬性有3取值可能:晴,陰,雨,我們發現當天氣為陰時,活動全為進行因此這件事情就可以確定了,而天氣為晴或雨時,活動中有進行的也有取消的,事件還無法確定,這時就需要在當前按照天氣屬性划分下的剩下的屬性中遞歸再次計算活動熵和信息增益,選擇信息增益最大的屬性來作為下一個節點,直到整個事件能夠確定下來。
例如當天氣為晴時,得到如下表所示的事件
天氣 |
溫度 |
濕度 |
風速 |
活動 |
晴 |
炎熱 |
高 |
弱 |
取消 |
晴 |
炎熱 |
高 |
強 |
取消 |
晴 |
適中 |
高 |
弱 |
取消 |
晴 |
寒冷 |
正常 |
弱 |
進行 |
晴 |
適中 |
正常 |
強 |
進行 |
我們需要遞歸處理,繼續在溫度,濕度,風速這三個屬性中找到信息增益最大的屬性來做為下一個節點。
首先繼續計算活動熵,此時有5個樣例,活動取消有3個,進行有2個,則活動熵為:
=0.971
接着計算信息增益,在天氣為晴的前提下,按照溫度屬性的不同取值分類后結果如下所示
天氣 |
溫度 |
濕度 |
風速 |
活動 |
晴 |
炎熱 |
高 |
弱 |
取消 |
晴 |
炎熱 |
高 |
強 |
取消 |
晴 |
適中 |
高 |
弱 |
取消 |
晴 |
寒冷 |
正常 |
弱 |
進行 |
晴 |
適中 |
正常 |
強 |
進行 |
發現濕度為高時有3種情況,活動取消有3種,進行有0種,則條件熵為:
=0
濕度正常有2種情況,活動取消0種,進行2中,則條件熵為:
=0
由於按照濕度屬性不同取值划分時,濕度為高占總情況的3/5,濕度正常占總情況的2/5,則按照濕度屬性不同取值划分時的帶權平均值熵為:,算出的結果約為0。
所以此時在天氣為晴的前提下,按照濕度屬性的不同取值划分的信息增益為:
Gain= H(活動|天氣=晴) - H(活動|天氣,濕度) = 0.971- 0=0.971
同理還需繼續計算在天氣為晴的前提下,按照溫度,風速屬性的不同取值划分的信息增益,找到信息增益最大的作為決策樹的下一個節點。
遞歸構造決策樹的代碼如下

1 #遞歸創建樹,用於找出出現次數最多的分類名稱 2 def majorityCnt(classList): 3 classCount={} 4 for vote in classList:#統計當前划分下每中情況的個數 5 if vote not in classCount.keys(): 6 classCount[vote]=0 7 classCount[vote]+=1 8 sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序 9 #對字典里的元素按照value值由大到小排序 10 print("****************") 11 print(sortedClassCount[0][0]) 12 return sortedClassCount[0][0] 13 14 15 def createTree(dataSet,labels): 16 classList=[example[-1] for example in dataSet]#創建數組存放所有標簽值,取dataSet里最后一列(結果) 17 #類別相同,停止划分 18 if classList.count(classList[-1])==len(classList):#判斷classList里是否全是一類,count() 方法用於統計某個元素在列表中出現的次數 19 return classList[-1] #當全是一類時停止分割 20 #長度為1,返回出現次數最多的類別 21 if len(classList[0])==1: #當沒有更多特征時停止分割,即分到最后一個特征也沒有把數據完全分開,就返回多數的那個結果 22 return majorityCnt(classList) 23 #按照信息增益最高選取分類特征屬性 24 bestFeat=chooseBestFeatureToSplit(dataSet)#返回分類的特征序號,按照最大熵原則進行分類 25 bestFeatLable=labels[bestFeat] #該特征的label, #存儲分類特征的標簽 26 27 myTree={bestFeatLable:{}} #構建樹的字典 28 del(labels[bestFeat]) #從labels的list中刪除該label 29 30 featValues=[example[bestFeat] for example in dataSet] 31 uniqueVals=set(featValues) 32 for value in uniqueVals: 33 subLables=labels[:] #子集合 ,將labels賦給sublabels,此時的labels已經刪掉了用於分類的特征的標簽 34 #構建數據的子集合,並進行遞歸 35 myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables) 36 return myTree
最后得到的決策樹如下圖所示
整個程序如下

1 from math import log 2 from operator import * 3 4 def storeTree(inputTree,filename): 5 import pickle 6 fw=open(filename,'wb') #pickle默認方式是二進制,需要制定'wb' 7 pickle.dump(inputTree,fw) 8 fw.close() 9 10 def grabTree(filename): 11 import pickle 12 fr=open(filename,'rb')#需要制定'rb',以byte形式讀取 13 return pickle.load(fr) 14 15 16 def createDataSet(): 17 ''' 18 dataSet=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']] 19 labels = ['no surfacing','flippers'] 20 ''' 21 dataSet = [['sunny','hot','high','weak','no'], 22 ['sunny','hot','high','strong','no'], 23 ['overcast','hot','high','weak','yes'], 24 ['rain','mild','high','weak','yes'], 25 ['rain','cool','normal','weak','yes'], 26 ['rain','cool','normal','strong','no'], 27 ['overcast','cool','normal','strong','yes'], 28 ['sunny','mild','high','weak','no'], 29 ['sunny','cool','normal','weak','yes'], 30 ['rain','mild','normal','weak','yes'], 31 ['sunny','mild','normal','strong','yes'], 32 ['overcast','mild','high','strong','yes'], 33 ['overcast','hot','normal','weak','yes'], 34 ['rain','mild','high','strong','no']] 35 labels = ['outlook','temperature','humidity','wind'] 36 return dataSet,labels 37 38 def calcShannonEnt(dataSet):#計算香農熵 39 numEntries = len(dataSet) 40 41 labelCounts = {} 42 for featVec in dataSet: 43 currentLabel = featVec[-1] #取得最后一列數據,該屬性取值情況有多少個 44 if currentLabel not in labelCounts.keys(): 45 labelCounts[currentLabel] = 0 46 labelCounts[currentLabel]+=1 47 48 #計算熵 49 shannonEnt = 0.0 50 for key in labelCounts: 51 prob = float(labelCounts[key])/numEntries 52 shannonEnt -= prob*log(prob,2) 53 54 return shannonEnt 55 56 #定義按照某個特征進行划分的函數splitDataSet 57 #輸入三個變量(待划分的數據集,特征,分類值) 58 #axis特征值中0代表no surfacing,1代表flippers 59 #value分類值中0代表否,1代表是 60 def splitDataSet(dataSet,axis,value): 61 retDataSet = [] 62 for featVec in dataSet:#取大列表中的每個小列表 63 if featVec[axis]==value: 64 reduceFeatVec=featVec[:axis] 65 reduceFeatVec.extend(featVec[axis+1:]) 66 retDataSet.append(reduceFeatVec) 67 68 return retDataSet #返回不含划分特征的子集 69 70 def chooseBestFeatureToSplit(dataSet): 71 numFeature = len(dataSet[0]) - 1 72 baseEntropy = calcShannonEnt(dataSet) 73 bestInforGain = 0 74 bestFeature = -1 75 76 for i in range(numFeature): 77 featList = [number[i] for number in dataSet]#得到某個特征下所有值(某列) 78 uniquelVals = set(featList) #set無重復的屬性特征值,得到所有無重復的屬性取值 79 80 #計算每個屬性i的概論熵 81 newEntropy = 0 82 for value in uniquelVals: 83 subDataSet = splitDataSet(dataSet,i,value)#得到i屬性下取i屬性為value時的集合 84 prob = len(subDataSet)/float(len(dataSet))#每個屬性取值為value時所占比重 85 newEntropy+= prob*calcShannonEnt(subDataSet) 86 inforGain = baseEntropy - newEntropy #當前屬性i的信息增益 87 88 if inforGain>bestInforGain: 89 bestInforGain = inforGain 90 bestFeature = i 91 92 return bestFeature#返回最大信息增益屬性下標 93 94 #遞歸創建樹,用於找出出現次數最多的分類名稱 95 def majorityCnt(classList): 96 classCount={} 97 for vote in classList:#統計當前划分下每中情況的個數 98 if vote not in classCount.keys(): 99 classCount[vote]=0 100 classCount[vote]+=1 101 sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序 102 #對字典里的元素按照value值由大到小排序 103 104 return sortedClassCount[0][0] 105 106 107 def createTree(dataSet,labels): 108 classList=[example[-1] for example in dataSet]#創建數組存放所有標簽值,取dataSet里最后一列(結果) 109 #類別相同,停止划分 110 if classList.count(classList[-1])==len(classList):#判斷classList里是否全是一類,count() 方法用於統計某個元素在列表中出現的次數 111 return classList[-1] #當全是一類時停止分割 112 #長度為1,返回出現次數最多的類別 113 if len(classList[0])==1: #當沒有更多特征時停止分割,即分到最后一個特征也沒有把數據完全分開,就返回多數的那個結果 114 return majorityCnt(classList) 115 #按照信息增益最高選取分類特征屬性 116 bestFeat=chooseBestFeatureToSplit(dataSet)#返回分類的特征序號,按照最大熵原則進行分類 117 bestFeatLable=labels[bestFeat] #該特征的label, #存儲分類特征的標簽 118 119 myTree={bestFeatLable:{}} #構建樹的字典 120 del(labels[bestFeat]) #從labels的list中刪除該label 121 122 featValues=[example[bestFeat] for example in dataSet] 123 uniqueVals=set(featValues) 124 for value in uniqueVals: 125 subLables=labels[:] #子集合 ,將labels賦給sublabels,此時的labels已經刪掉了用於分類的特征的標簽 126 #構建數據的子集合,並進行遞歸 127 myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables) 128 return myTree 129 130 131 if __name__=="__main__": 132 my_Data,labels = createDataSet() 133 134 #print(calcShannonEnt(my_Data)) 135 Mytree = createTree(my_Data,labels) 136 print(Mytree)