基本流程
決策樹是通過分次判斷樣本屬性來進行劃分樣本類別的機器學習模型。每個樹的結點選擇一個最優屬性來進行樣本的分流,最終將樣本類別劃分出來。
決策樹的關鍵就是分流時最優屬性$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")