Decision tree——決策樹

頎周發表於2020-04-30

基本流程

  決策樹是通過分次判斷樣本屬性來進行劃分樣本類別的機器學習模型。每個樹的結點選擇一個最優屬性來進行樣本的分流,最終將樣本類別劃分出來。  

  決策樹的關鍵就是分流時最優屬性$a$的選擇。使用所謂資訊增益$Gain(D,a)$來判別不同屬性的劃分效能,即劃分前樣本類別的資訊熵,減去劃分後樣本類別的平均資訊熵,顯然資訊增益越大越好:

$\text{Ent}(D)=-\sum\limits_{k=1}^{|\mathcal{Y}|}p_k\log_{2}p_k$
$\displaystyle\text{Gain}(D,a)=\text{Ent}(D)-\sum\limits_{v=1}^{V}\frac{|D^v|}{|D|}\text{Ent}(D^v)$

  其中$D$是劃分前的資料集,$|\mathcal{Y}|$是樣本的類別數,$p_k$是資料集中類別$k$的比例,$D^v$是劃分後的某個資料集,$V$是資料集的分流數量。

  又考慮到可能有的屬性取值過多,直接將樣本劃分為多個只包含一個樣本的集合,資訊熵變為了0。如此似乎取得最大的資訊增益,但實際上是過擬合了。因此,還要使用“增益率”來平衡,除了資訊增益要大外,劃分出的集合數要小。增益率定義如下:

$\text{Gain_ratio}(D,a)=\displaystyle \frac{\text{Gain(D,a)}}{\text{IV}(a)},$

$\displaystyle\text{IV}(a)=-\sum\limits_{v=1}^V\frac{|D^v|}{|D|}\log_{2}\frac{|D^v|}{|D|}$

  另外,也不能一味地取增益率大的屬性,因為大增益率偏好屬性種類少的屬性,也就會偏好連續屬性(因為連續屬性是取一個劃分點來將樣本劃分為兩部分,而離散屬性則可能有多個屬性種類)。因此通常會啟發性地先選出資訊增益大於平均值的屬性,再從其中選擇增益率最大的屬性。

實驗

  訓練資料集使用西瓜資料集:

  實驗沒有使用python的機器學習包sklearn,分別測試了使用與不使用增益率來生成決策樹。 首先自定義樹結點的結構,分別是離散屬性結點、連續屬性結點與葉結點,如下:

 1 node(離散):
 2 { 
 3     "divide_attr": ["紋理", 3, 0, 0],        //0:屬性名稱(第幾個屬性) //1:屬性序號  //2:0離散,1連續     //3:連續屬性的劃分點
 4     "if_leave": false,            //是否為葉結點 
 5     "info_gain": 0.3805918973682686,    //資訊增益
 6     "gain_ratio": 0.2630853587192754,    //資訊率
 7     "divide": 
 8     {
 9         "清晰":node,
10         "稍糊":node,
11         "模糊":node
12     }//存各個樣式的結點
13 }
14 node(連續):
15 { 
16     "divide_attr": ["密度", 6, 1, 0.3815],        
17     "if_leave": false,            
18     "info_gain": 0.7642045065086203,    
19     "gain_ratio": 1.0,    
20     "divide": 
21     {
22         "0":node,            //小於等於劃分點
23         "1":node            //大於劃分點
24     }//存各個樣式的結點 
25 }
26 node(葉結點):
27 {
28     "if_leave":true,
29     "class":""            //判斷類別
30     "samples":[...]            //存生成決策樹時劃分到這個葉結點的樣本
31 }

  結點使用字典儲存。

  將資料輸入Excel中並在python中讀入,然後使用處理好的資料生成決策樹。以下是不使用增益率生成的決策樹結構:

  以下是使用增益率生成的決策樹結構:

  對比可以發現,當增益率參與決策樹的生成時,連續屬性會優先被使用。使用以上二者進行對訓練集進行測試的正確率都是1.0。以下是處理資料、生成決策樹、訓練集驗證、畫出決策樹結構的程式碼:

  1 #%%
  2 import matplotlib as plt
  3 import numpy as np
  4 import xlrd
  5 import sys
  6 
  7 table = xlrd.open_workbook('data.xlsx').sheets()[0]#讀取Excel資料
  8 data = []
  9 for i in range(0,table.nrows):
 10     data.append(table.row_values(i))  
 11  
 12 attr_type = np.zeros([len(data[0])-2])#獲取屬性型別0離散,1連續
 13 for i in range(len(attr_type)):
 14     if type(data[1][i+1]) == str:
 15         attr_type[i] = 0
 16     else:
 17         attr_type[i]=1
 18 
 19 data = np.array(data)[:,1:] #轉為數字矩陣 並去掉序號
 20 all_attr = data[0,:-1]  #存屬性名稱
 21 data = data[1:]#去掉表頭   
 22    
 23 #%%
 24 def get_info_entropy(a):
 25     """
 26     傳入array或list計算類別的資訊熵
 27     """
 28     c = {}
 29     n = len(a)
 30     for i in a:
 31         if i not in c.keys(): 
 32             c[i] = 1
 33         else:
 34             c[i] += 1
 35     entropy = 0
 36     for i in c.keys():
 37         p = c[i]/n
 38         entropy += -p*np.log2(p) 
 39     return entropy  
 40 
 41 def info_gain_and_ratio(D,s):
 42     """
 43     傳入原資料集、按屬性分類後的字典s
 44     """
 45     info_gain  = get_info_entropy(D[:,-1])
 46     class_entro = 0 
 47     for i in s.keys():
 48         n = len(s[i])
 49         info_gain -= n/len(D)*get_info_entropy(s[i][:,-1])
 50         class_entro-=n/len(D)*np.log2(n/len(D))
 51     if class_entro == 0:
 52         return info_gain,info_gain
 53     return info_gain,info_gain/class_entro
 54     
 55 
 56 def attr_classfier(D,an,if_dic): 
 57     """ 
 58     傳入:資料集、分類屬性序號、是否傳出字典
 59     使用屬性對D進行分類
 60     傳出:
 61     1、離散:以屬性值為key,以分類後的資料集為value的字典dictionary 
 62     連續:key為0時<bound,為1時>bound
 63     2、連續屬性的最優分界點float,離散的傳出0
 64     3、類別資訊增益
 65     4、增益率
 66     """
 67     dic = {}
 68     opt_bound = 0
 69     info_gain = 0
 70     gain_ratio = 0
 71     if attr_type[an] == 0:#離散屬性獲得分類資料集
 72         for i in D:
 73             if i[an] not in dic.keys():
 74                 dic[i[an]] = [i]
 75             else:
 76                 dic[i[an]].append(i)
 77         for i in dic.keys():
 78             dic[i] = np.array(dic[i]) 
 79         info_gain,gain_ratio = info_gain_and_ratio(D,dic)
 80     elif attr_type[an] == 1:#連續屬性獲得分類資料集
 81         attrs = D[:,an]
 82         attrs = np.sort(attrs.astype(float))
 83         for i in range(len(attrs)-1): 
 84             bound = (attrs[i]+attrs[i+1])/2
 85             dic0 = {} #每次都初始化
 86             dic0['0'] = []
 87             dic0['1'] = [] 
 88             for j in D:
 89                 if float(j[an]) <= bound:
 90                     dic0['0'].append(j)
 91                 else:
 92                     dic0['1'].append(j) 
 93             for j in dic0.keys():
 94                 dic0[j] = np.array(dic0[j])
 95             t,b = info_gain_and_ratio(D,dic0) 
 96             if t>info_gain:
 97                 dic = dic0
 98                 opt_bound = bound
 99                 info_gain = t
100                 gain_ratio = b
101     if if_dic:
102         return dic,opt_bound,info_gain,gain_ratio
103     return opt_bound,info_gain,gain_ratio  
104 
105 def get_most_class(d):
106     """
107     獲取資料集中佔比最大的類別
108     """
109     c = {}  
110     for i in d[:,-1]:
111         if i not in c.keys(): 
112             c[i] = 1
113         else:
114             c[i] += 1
115     m = ""
116     for i in c.keys():
117         if m == "":
118             m = i
119         elif c[i] > c[m]:
120             m = i
121     return m
122             
123 #%%
124 def get_opt_attr(ave_info_gain,info_gains,gain_ratios,A,use_gain_ratios):
125     """
126     獲取最優屬性傳入:
127     1、平均資訊增益
128     2、所有屬性的資訊增益
129     3、所有屬性的資訊率
130     4、屬性可用list
131     5、是否使用資訊率
132     """
133     opt_attr_index = 0
134     #獲取最優屬性 
135     for i in range(len(A)):
136         if A[i] == 1:
137             if info_gains[i] > ave_info_gain:#在資訊增益大於平均中取最大資訊率
138                 if use_gain_ratios:
139                     if gain_ratios[i] > gain_ratios[opt_attr_index]:
140                         opt_attr_index = i ################取到最優屬性了 
141                 else:
142                     if info_gains[i] > info_gains[opt_attr_index]:
143                         opt_attr_index = i
144     return opt_attr_index
145 
146 def create_node(D,A,use_gain_ratios):
147     '''
148     :傳入資料集和屬性集
149     :D傳入資料集的切片
150     :A傳入屬性的使用矩陣,如[1,1,1,0,0,0,1],1表示可使用,0表示已使用
151     :函式同一類別的先判斷,之後屬性取值全相同和劃分屬性放一起
152     '''
153     node = {}
154     if len(set(D[:,-1])) == 1:#類別全相等,葉結點
155         node["if_leave"]=True
156         node["class"]=D[0,-1]
157         node["samples"] = D.tolist()
158         return node 
159     info_gains = np.zeros([len(A)]) #所有可用屬性得出的資訊增益
160     ave_info_gain = 0#平均資訊增益
161     gain_ratios = np.zeros([len(A)])#所有可用屬性得出的資訊增益率
162     opt_attr_index = 0#大於平均資訊增益的屬性中,增益率最大的屬性索引
163     attr_bound = np.zeros([len(A)])   #連續屬性的屬性界限
164     active_attrN = 0 #可用屬性數,用於求資訊增益平均 
165     for i in range(len(A)):
166         if A[i] == 1:
167             attr_bound[i],info_gains[i],gain_ratios[i] =  attr_classfier(D,i,False)
168             ave_info_gain += info_gains[i]
169             active_attrN += 1 
170     """
171     以下判斷之一成立,即為葉結點,沒有分下去的意義:
172     # 1、所有屬性增益率都太低  
173     # 2、所有屬性是否分別在所有樣本上取值都相同(同上,資訊增益=0)  
174     # 3、可用屬性為空
175     """
176     if ave_info_gain < 0.01 or active_attrN == 0:
177         node["if_leave"] = True
178         node["class"] = get_most_class(D[:,-1])#類別為資料集中最多的類
179         node["samples"] = D.tolist()
180         return node
181     #獲取最優屬性
182     opt_attr_index = get_opt_attr(opt_attr_index,info_gains,gain_ratios,A,use_gain_ratios) 
183     """
184     以下由最優屬性生成子結點
185     """
186     dic,bound,info_gain,gain_ratio= attr_classfier(D,opt_attr_index,True)
187     if attr_type[opt_attr_index] == 0:#離散
188         A[opt_attr_index] = 0
189         node["divide_attr"] = [all_attr[opt_attr_index],opt_attr_index,0,0] 
190     elif attr_type[opt_attr_index] == 1:#連續
191         node["divide_attr"] = [all_attr[opt_attr_index],opt_attr_index,1,bound]
192     sons = {}
193     for i in dic.keys():
194         sons[i] = create_node(dic[i],A[:],use_gain_ratios) 
195     node["if_leave"] = False
196     node["info_gain"] = info_gain
197     node["gain_ratio"] = gain_ratio    
198     node["divide"] = sons
199     return node
200 
201 """
202 此處生成決策樹,True使用增益率,False不用
203 """
204 root = create_node(data,np.ones([len(all_attr)]),False)
205 
206 #%%
207 """
208 以上訓練好模型root,下面測試
209 """
210 def test_decision_tree(sample,tree):
211     decision = ""
212     while True:
213         if tree["if_leave"] == True:
214             decision = tree["class"]
215             break
216         if tree["divide_attr"][2] == 0:#離散
217             attr = tree["divide_attr"][1] 
218             tree = tree["divide"][sample[attr]]
219         elif tree["divide_attr"][2] == 1:#連續
220             attr = tree["divide_attr"][1] 
221             b = tree["divide_attr"][3]
222             if float(sample[attr]) <= b:
223                 tree = tree["divide"]["0"]
224             else:
225                 tree = tree["divide"]["1"]
226     return decision 
227 right = 0
228 for i in data: 
229     a = test_decision_tree(i,root) 
230     if i[-1] == a:
231         right +=1
232 print("正確率:" + str(right/len(data))) 
233 #%%
234 """
235 Json匯出樹的結構
236 """
237 import json
238 with open('decision tree.json','w',encoding='utf-8') as f:
239     f.write(json.dumps(root,ensure_ascii = False))
240 #%%
241 """
242 畫出決策樹結構
243 """ 
244 import pydotplus as pdp 
245 
246 def iterate_tree(tree,num): 
247     """
248     迭代決策樹,遞迴出結點間的箭頭map
249     """
250     map_str = ""
251     itenum = num 
252     if tree["if_leave"]:
253         map_str = str(num)+'[label="' + tree["class"] + '"];' #類別
254         map_str += str(num)+'[shape=ellipse];' #顯示為橢圓
255     else: 
256         if tree["divide_attr"][2] == 0:#離散屬性
257             map_str = str(num)+'[label="' + tree["divide_attr"][0] + '=?"];' #判別屬性
258             for i in tree["divide"].keys():
259                 itenum+=1
260                 map_str += str(num)+"->"+str(itenum)+'[label="'+ i +'"];' #新增邊與邊標籤
261                 son_map_str, itenum= iterate_tree(tree["divide"][i],itenum)
262                 map_str+=son_map_str
263         elif tree["divide_attr"][2] == 1:#連續屬性 
264             map_str = str(num)+'[label="' + tree["divide_attr"][0] +"<="+ str(tree["divide_attr"][3]) + '?"];' #判別屬性標籤
265             itenum+=1
266             map_str += str(num)+"->"+str(itenum)+'[label="是"];' #新增邊與邊標籤
267             son_map_str, itenum= iterate_tree(tree["divide"]["0"],itenum)
268             map_str+=son_map_str
269             itenum+=1
270             map_str += str(num)+"->"+str(itenum)+'[label="否"];' #新增邊與邊標籤
271             son_map_str, itenum= iterate_tree(tree["divide"]["1"],itenum)
272             map_str+=son_map_str
273 
274     return map_str,itenum
275 def get_decision_tree_map(tree):
276     map_str = """
277     digraph decision{
278         node [shape=box, style="rounded", color="black", fontname="Microsoft YaHei"]; 
279         edge [fontname="Microsoft YaHei"];   
280     """
281     mm,n = iterate_tree(tree,0) 
282     return map_str + mm + "}"  
283 
284 decision_tree_map =  get_decision_tree_map(root)
285 print(decision_tree_map)
286 graph = pdp.graph_from_dot_data(decision_tree_map)
287 graph.write_pdf("Decision tree.pdf") 

相關文章