【10月31日】機器學習實戰(二)決策樹:隱形眼鏡資料集
阿新 • • 發佈:2019-02-02
決策樹的優點:計算的複雜度不高,輸出的結果易於理解,對中間值的確實不敏感,可以處理不相關的特徵資料
決策樹的缺點:可能會產生過度匹配的問題。
其本質的思想是通過尋找區分度最好的特徵(屬性),用於支援分類規則的制定。
那麼哪些特徵是區分度好的,哪些特徵是區分度壞的呢?換句話說,如何衡量資料集中特徵(屬性)對例項的區分程度呢?
依據夏農的資訊理論,引入資訊熵的思想作為對特徵區分程度的度量。當然,資訊熵並不是唯一的度量指標,在一些機器學習的開源包中,也提供了別的依據。跟著書走,本次實驗還是選擇資訊熵作為劃分資料集的標準。原書《機器學習實戰》中使用的是python2,本人在python3實現,略有不同。
程式碼如下。
shannon.py
tree_plot.pyfrom math import log import operator # 計算資訊熵 def clacShannon(data): num = len(data) # 用於計算每個類別出現的次數,配合num就可以計算該類別的概率了 label_count = {} for feat_vec in data: # 向量的最後一個為標籤(類別) current_label = feat_vec[-1] if current_label in label_count.keys(): label_count[current_label] += 1 else: label_count[current_label] = 1 entropy = 0.0 for key in label_count.keys(): prob = float(label_count[key]) / num entropy -= prob * log(prob, 2) return entropy # 劃分資料集 # data 需要劃分的資料,雙重列表[[],[],……,[]] # axis 劃分的特徵 # value 上述axis的值 def splitData(data, axis, value): result = [] for feature_vec in data: if feature_vec[axis] == value: # 將符合條件的例項加入到result中(並去除了相應的特徵) # result.append(feature_vec[:axis].extend(feature_vec[axis + 1:])) reduce_feat = feature_vec[:axis] reduce_feat.extend(feature_vec[axis+1:]) result.append(reduce_feat) return result # 選擇分類效果最好的特徵 def chooseFeature(data): num_data = len(data) num_feature = len(data[0]) - 1 # 原始資料的夏農熵 base_entropy = clacShannon(data) # 資訊增益 best_info_gain = 0.0 # 分類效果最好的特徵 best_feature = -1 # 計算各個特徵的資訊增益 for i in range(num_feature): # 獲取特徵i下所有可能的取值,並去重 feature_list = [example[i] for example in data] values = set(feature_list) # 新的夏農熵 new_entropy = 0.0 for value in values: sub_data = splitData(data, i, value) prob = len(sub_data) / float(num_data) new_entropy += prob * clacShannon(sub_data) info_gain = base_entropy - new_entropy # 選擇分類效果最好--資訊增益最大 if info_gain > best_info_gain: best_info_gain = info_gain best_feature = i return best_feature # 使用投票機制確定節點的類別 def majorityCnt(class_list): class_count = {} for vote in class_list: if vote not in class_count.keys(): class_count[vote] = 0 class_count[vote] += 1 # 選取票數最多的作為分類作為該節點的最終類別 sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reversed=True) return sorted_class_count[0][0] # 建立決策樹 # data 資料集,每條記錄的最後一項為該例項的類別 # labels 為了增加結果的可解釋性,設定標籤 def createTree(data, labels): # data中每條記錄的最後一項為該例項的類別 class_list = [example[-1] for example in data] # 結束條件一:該分支下所有記錄的類別相同,則為葉子節點,停止分類 if class_list.count(class_list[0]) == len(class_list): return class_list[0] # 結束條件二:所有特徵使用完畢,該節點為葉子節點,節點類別投票決定 if len(data[0]) == 1: return majorityCnt(class_list) best_feature = chooseFeature(data) best_label = labels[best_feature] my_tree = {best_label: {}} del(labels[best_feature]) feature_values = [example[best_feature] for example in data] unique_values = set(feature_values) for value in unique_values: sub_lables = labels[:] # 遞迴 my_tree[best_label][value] = createTree(splitData(data, best_feature, value), sub_lables) return my_tree # 使用決策樹進行分類 # 引數說明:決策樹, 標籤, 待分類資料 def classify(input_tree, feature_labels, test_vec): first_str = input_tree.keys()[0] second_dict = input_tree[first_str] # 得到第特徵的索引,用於後續根據此特徵的分類任務 feature_index = feature_labels.index(first_str) for key in second_dict.keys(): if test_vec[feature_index] == key: if type(second_dict[key]).__name__ == 'dict': classLabel = classify(second_dict[key], feature_labels, test_vec) # 達到葉子節點,返回遞迴呼叫,得到分類 else: classLabel = second_dict[key] return classLabel # 決策樹的儲存 # 決策樹的構造是一個很耗時的過程,因此需要將構造好的樹儲存起來以備後用 # 使用pickle序列化物件 def storeTree(input_tree, filename): import pickle fw = open(filename, "w") pickle.dump(input_tree, fw) fw.close() # 讀取檔案中的決策樹 def grabTree(filename): import pickle fr = open(filename) return pickle.load(fr)
測試程式碼import matplotlib.pyplot as plt # 用字典進行儲存 # boxstyle為文字框屬性, 'sawtooth':鋸齒型;fc為邊框粗細 decision_node = dict(boxstyle='sawtooth', fc='0.8') leaf_node = dict(boxstyle='round4', fc='0.8') arrow_args = dict(arrowstyle='<-') # node_txt 要註解的文字,center_pt文字中心點,箭頭指向的點,parent_pt箭頭的起點 def plotNode(node_txt, center_pt, parent_pt, node_type): createPlot.ax1.annotate(node_txt, xy=parent_pt, xycoords='axes fraction', xytext=center_pt, textcoords='axes fraction', va="center", ha="center", bbox=node_type, arrowprops=arrow_args) # 建立畫板 def createPlot(in_tree): # figure建立畫板,‘1’表示第一個圖,背景為白色 fig = plt.figure(1, facecolor='white') # 清空畫板 fig.clf() axprops = dict(xticks=[], yticks=[]) # subplot(x*y*z),表示把畫板分割成x*y的網格,z是畫板的標號, # frameon=False表示不繪製座標軸矩形 createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) # plotNode('decision_node', (0.5, 0.1), (0.1, 0.5), decision_node) # plotNode('leaf_node', (0.8, 0.1), (0.8, 0.3), leaf_node) # plt.show() # 儲存樹的寬度 plotTree.totalW = float(getNumLeafs(in_tree)) # 儲存樹的深度 plotTree.totalD = float(getTreeDepth(in_tree)) # xOff用於追蹤已經繪製的節點的x軸位置資訊,為下一個節點的繪製提供參考 plotTree.xOff = -0.5/plotTree.totalW # yOff用於追蹤已經繪製的節點y軸的位置資訊,為下一個節點的繪製提供參考 plotTree.yOff = 1.0 plotTree(in_tree, (0.5, 1.0), '') plt.show() # 為了繪製樹,要先清楚葉子節點的數量以及樹的深度--以便確定x軸的長度和y軸的高度 # 下面就分別定義這兩個方法 def getNumLeafs(my_tree): num_leafs = 0 first_str = next(iter(my_tree)) # 找到第一個節點 second_dic = my_tree[first_str] # 測試節點資料是否為字典型別,葉子節點不是字典型別 for key in list(second_dic.keys()): # 如果節點為字典型別,則遞迴使用getNumLeafs() if type(second_dic[key]).__name__ == 'dict': num_leafs += getNumLeafs(second_dic[key]) else: num_leafs += 1 return num_leafs def getTreeDepth(my_tree): max_depth = 0 first_str = next(iter(my_tree)) second_dic = my_tree[first_str] # 測試節點資料是否為字典型別,葉子節點不是字典型別 for key in list(second_dic.keys()): # 如果節點為字典型別,遞迴使用getTreeDepth() if type(second_dic[key]).__name__ == 'dict': this_depth = 1 + getTreeDepth(second_dic[key]) else: # 當節點不為字典型,為葉子節點,深度遍歷結束 # 從遞迴中呼叫返回,且深度加1 this_depth = 1 # 最大的深度儲存在max_depth中 if this_depth > max_depth: max_depth = this_depth return max_depth # 在父子節點之間填充文字資訊進行標註 # 在決策樹中此處應是對應父節點的屬性值 def plotMidText(center_pt, parent_pt, txt_string): x_mid = (parent_pt[0] - center_pt[0])/2.0 + center_pt[0] y_mid = (parent_pt[1] - center_pt[1])/2.0 + center_pt[1] createPlot.ax1.text(x_mid, y_mid, txt_string) def plotTree(my_tree, parent_pt, node_txt): num_leafs = getNumLeafs(my_tree) depth = getTreeDepth(my_tree) first_str = list(my_tree.keys())[0] # 以第一次呼叫為例說明 # 此時 繪製的為根節點,根節點的x軸:-0.5/plotTree.totalW + (1.0 + float(num_leafs))/2.0/plotTree.totalW # 假設整個樹中葉子節點的數目為6 則上述根節點的x軸:-0.5/6 + (1 + 6)/2.0/6 = 0.5 # 實際上,對於根節點而言,下式的值始終是0.5 center_pt = (plotTree.xOff + (1.0 + float(num_leafs))/2.0/plotTree.totalW, plotTree.yOff) plotMidText(center_pt, parent_pt, node_txt) plotNode(first_str, center_pt, parent_pt, decision_node) second_dict = my_tree[first_str] # y軸的偏移--深度優先的繪製策略 plotTree.yOff -= 1.0 / plotTree.totalD for key in list(second_dict.keys()): if type(second_dict[key]).__name__ == 'dict': plotTree(second_dict[key], center_pt, str(key)) else: plotTree.xOff += 1.0 / plotTree.totalW plotNode(second_dict[key], (plotTree.xOff, plotTree.yOff), center_pt, leaf_node) plotMidText((plotTree.xOff, plotTree.yOff), center_pt, str(key)) plotTree.yOff += 1.0 / plotTree.totalD
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lenses_labels = ['age', 'prescript', 'astigmatic', 'tear_rate']
lenses_tree = shannon.createTree(lenses, lenses_labels)
print(lenses_tree)
tree_plotter.createPlot(lenses_tree)
實驗結果: