目錄node
讓咱們來玩一個遊戲,你如今在你的腦海裏想好某個事物,你的同桌向你提問,可是隻容許問你20個問題,你的回答只能是對或錯,若是你的同桌在問你20個問題以前說出了你腦海裏的那個事物,則他贏,不然你贏。懲罰,你本身定咯。python
決策樹的工做原理就相似與咱們這個遊戲,用戶輸入一些列數據,而後給出遊戲的答案。算法
決策樹聽起來很高大善,其實它的概念很簡單。經過簡單的流程就能理解其工做原理。數據庫
圖3-1 決策樹流程圖數據結構
從圖3-1咱們能夠看到一個流程圖,他就是一個決策樹。長方形表明判斷模塊,橢圓形表明終止模塊,從判斷模塊引出的左右箭頭稱做分支。該流程圖構造了一個假想的郵件分類系統,它首先檢測發送郵件域名地址。若是地址爲 myEmployer.com,則將其放在分類「無聊時須要閱讀的郵件」;若是郵件不是來自這個域名,則檢查郵件內容裏是否包含單詞 曲棍球 ,若是包含則將郵件歸類到「須要及時處理的朋友郵件」;若是不包含則將郵件歸類到「無需閱讀的垃圾郵件」。app
第二章咱們已經介紹了 k-近鄰算法完成不少分類問題,可是它沒法給出數據的內在含義,而決策樹的主要優點就在此,它的數據形式很是容易理解。函數
那如何去理解決策樹的數據中所蘊含的信息呢?接下來咱們就將學習如何從一堆原始數據中構造決策樹。首先咱們討論構造決策樹的方法,以及如何比那些構造樹的 Python 代碼;接着提出一些度量算法成功率的方法;最後使用遞歸創建分類器,而且使用 Matplotlib 繪製決策樹圖。構造完成決策樹分類器以後,咱們將輸入一些隱形眼睛的處方數據,並由決策樹分類器預測須要的鏡片類型。工具
優勢: 計算複雜度不高,輸出結果易於理解,對中間值的缺失不敏感,能夠處理不相關特徵數據。 缺點: 可能會產生過分匹配問題。
在構造決策樹的時候,咱們將會遇到的第一個問題就是找到決定性的特徵,劃分出最好的結果,所以咱們必須評估每一個特徵。學習
# 構建分支的僞代碼函數 create_branch 檢測數據集中的每一個子項是否屬於同一分類: if yes return 類標記 else 尋找劃分數據集的最好特徵 劃分數據集 建立分直節點 for 每一個劃分的子集 調用函數 create_branch 並增長返回結果到分支節點 return 分支節點
在構造 create_branch 方法以前,咱們先來了解一下決策樹的通常流程:測試
1. 收集數據: 可使用任何方法 2. 準備數據: 樹構造算法只適用於標稱型數據,所以數值型數據必須離散化 3. 分析數據: 可使用任何方法,構造樹完成以後,咱們應該檢查圖形是否符合預期 4. 訓練算法: 構造樹的數據結構 5. 測試算法: 使用經驗樹計算錯誤率 6. 使用算法: 此步驟能夠適用於任何監督學習算法,而使用決策樹能夠更好的理解數據的內在含義
一些決策樹採用二分法劃分數據,本書並不採用這種方法。若是依據某個屬性劃分數據將會產生4個可能的值,咱們將把數據劃分紅四塊,並建立四個不一樣的分支。本書將採用 ID3 算法劃分數據集。關於 ID3 算法的詳細解釋。
表3-1 海洋生物數據
不浮出水面是否能夠生存 | 是否有腳蹼 | 屬於魚類 | |
---|---|---|---|
1 | 是 | 是 | 是 |
2 | 是 | 是 | 是 |
3 | 是 | 否 | 否 |
4 | 否 | 是 | 否 |
5 | 否 | 是 | 否 |
劃分大數據的大原則是:將無序的數據變得更加有序。其中組織雜亂無章數據的一種方法就是使用信息論度量信息。信息論是量化處理信息的分支科學。咱們能夠在劃分數據以前或以後使用信息論量化度量信息的內容。其中在劃分數據以前以後信息發生的變化稱爲信息增益,得到信息增益最高的特徵就是最好的選擇。
在數據劃分以前,咱們先介紹熵也稱做香農熵。熵定義爲信息的指望值,它是信息的一種度量方式。
信息——若是待分類的事務可能劃分在多個分類之中,則符號\(x_i\)的信息定義爲:
\(l(x_i)=-log_2p(x_i)\) \(p(x_i)\)是選擇該分類的機率
全部類別全部可能包含的信息指望值:
\(H=-\sum_{i=1}^np(x_i)log_2p(x_i)\)n是分類的數目
對公式有必定的瞭解以後咱們在 trees.py 文件中定義一個 calc_shannon_ent 方法來計算信息熵。
# trees.py from math import log def calc_shannon_ent(data_set): # 計算實例總數 num_entries = len(data_set) label_counts = {} # 統計全部類別出現的次數 for feat_vec in data_set: current_label = feat_vec[-1] if current_label not in label_counts.keys(): label_counts[current_label] = 0 label_counts[current_label] += 1 # 經過熵的公式計算香農熵 shannon_ent = 0 for key in label_counts: # 統計全部類別出現的機率 prob = float(label_counts[key] / num_entries) shannon_ent -= prob * log(prob, 2) return shannon_ent def create_data_set(): """構造咱們以前對魚鑑定的數據集""" data_set = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] labels = ['no surfacing', 'flippers'] return data_set, labels def shannon_run(): data_set, labels = create_data_set() shannon_ent = calc_shannon_ent(data_set) print(shannon_ent) # 0.9709505944546686 if __name__ == '__main__': shannon_run()
混合的數據越多,則熵越高,有興趣的同窗能夠試試增長一個 ’maybe’ 的標記。
若是還有興趣的能夠自行了解另外一個度量集合無序程度的方法是基尼不純度,簡單地講就是從一個數據集中隨機選取子項,度量其被錯誤分類到其餘分組裏的機率。
咱們已經測量了信息熵,可是咱們還須要劃分數據集,度量劃分數據集的熵,所以判斷當前是否正確地劃分數據集。你能夠想象一個二維空間,空間上隨機分佈的數據,咱們如今須要在數據之間劃一條線把它們分紅兩部分,那咱們應該按照 x 軸仍是 y 軸劃線呢?
所以咱們能夠定義一個 split_data_set 方法來解決上述所說的問題:
# trees.py def split_data_set(data_set, axis, value): # 對符合規則的特徵進行篩選 ret_data_set = [] for feat_vec in data_set: # 選取某個符合規則的特徵 if feat_vec[axis] == value: # 若是符合特徵則刪掉符合規則的特徵 reduced_feat_vec = feat_vec[:axis] reduced_feat_vec.extend(feat_vec[axis + 1:]) # 把符合規則的數據寫入須要返回的列表中 ret_data_set.append(reduced_feat_vec) return ret_data_set def split_data_run(): my_data, labels = create_data_set() ret_data_set = split_data_set(my_data, 0, 1) print(ret_data_set) # [[1, 'yes'], [1, 'yes'], [0, 'no']] if __name__ == '__main__': # shannon_run() split_data_run()
接下來咱們把上述兩個方法結合起來遍歷整個數據集,找到最好的特徵劃分方式。
所以咱們須要定義一個 choose_best_feature_to_split 方法來解決上述所說的問題:
# trees.py def choose_best_feature_to_split(data_set): # 計算特徵數量 num_features = len(data_set[0]) - 1 # 計算原始數據集的熵值 base_entropy = calc_shannon_ent(data_set) best_info_gain = 0 best_feature = -1 for i in range(num_features): feat_list = [example[i] for example in data_set] # 遍歷某個特徵下的全部特徵值按照該特徵的權重值計算某個特徵的熵值 unique_vals = set(feat_list) new_entropy = 0 for value in unique_vals: sub_data_set = split_data_set(data_set, i, value) prob = len(sub_data_set) / float(len(data_set)) new_entropy += prob * calc_shannon_ent(sub_data_set) # 計算最好的信息增益 info_gain = base_entropy - new_entropy if info_gain > best_info_gain: best_info_gain = info_gain best_feature = i return best_feature def choose_best_feature_run(): data_set, _ = create_data_set() best_feature = choose_best_feature_to_split(data_set) print(best_feature) # 0 if __name__ == '__main__': # shannon_run() # split_data_run() choose_best_feature_run
咱們發現第0個即第一個特徵是最好的用於劃分數據集的特徵。對照表3-1,咱們能夠發現若是咱們以第一個特徵劃分,特徵值爲1的海洋生物分組將有兩個屬於魚類,一個屬於非魚類;另外一個分組則所有屬於非魚類。若是按照第二個特徵劃分……能夠看出以第一個特徵劃分效果是較好於以第二個特徵劃分的。同窗們也能夠用 calc_shannon_entropy 方法測試不一樣特徵分組的輸出結果。
咱們已經介紹了構造決策樹所須要的子功能模塊,其工做原理以下:獲得原始數據集,而後基於最好的屬性值劃分數據集,因爲特徵值可能多於兩個,所以可能存在大於兩個分支的數據集劃分。第一次劃分後,數據將被向下傳遞到樹分支的下一個節點,在這個節點上,咱們能夠再次劃分數據。所以咱們採用遞歸的原則處理數據集。
遞歸結束的條件是:程序遍歷完全部劃分數據集的屬性,或者每一個分支下的全部實例都具備相同的分類。若是全部實例具備相同的分類,則獲得一個葉子節點或者終止塊。任何到達葉子節點的數據必然屬於葉子節點的分類。如圖3-2 所示:
圖3-2 劃分數據集時的數據路徑
第一個結束條件使得算法能夠終止,咱們甚至能夠設置算法能夠劃分的最大分組數目。後續章節會陸續介紹其餘決策樹算法,如 C4.5和 CART,這些算法在運行時並不老是在每次劃分分組時都會消耗特徵。因爲特徵數目並非在每次劃分數據分組時都減小,所以這些算法在實際使用時可能引發必定的問題。但目前咱們並不須要考慮這個問題,咱們只須要查看算法是否使用了全部屬性便可。若是數據集已經處理了全部特徵,可是該特徵下的類標記依然不是惟一的,可能類標記爲是,也可能爲否,此時咱們一般會採用多數表決的方法決定該葉子節點的分類。
所以咱們能夠定義一個 maority_cnt 方法來決定如何定義葉子節點:
# trees.py def majority_cnt(class_list): """對 class_list 作分類處理,相似於 k-近鄰算法的 classify0 方法""" import operator 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), reverse=True) return sorted_class_count[0][0]
在定義處理葉子節點的方法以後,咱們就開始用代碼 create_tree 方法實現咱們的整個流程:
# trees.py def create_tree(data_set, labels): # 取出數據集中的標記信息 class_list = [example[-1] for example in data_set] # 若是數據集中的標記信息徹底相同則結束遞歸 if class_list.count(class_list[0]) == len(class_list): return class_list[0] # TODO 補充解釋 # 若是最後使用了全部的特徵沒法將數據集劃分紅僅包含惟一類別的分組 # 所以咱們遍歷完全部特徵時返回出現次數最多的特徵 if len(data_set[0]) == 1: return majority_cnt(class_list) # 經過計算熵值返回最適合劃分的特徵 best_feat = choose_best_feature_to_split(data_set) best_feat_label = labels[best_feat] my_tree = {best_feat_label: {}} del (labels[best_feat]) # 取出最合適特徵對應的值並去重即找到該特徵對應的全部可能的值 feat_values = [example[best_feat] for example in data_set] unique_vals = set(feat_values) # 遍歷最適合特徵對應的全部可能值,而且對這些可能值繼續生成子樹 # 相似於 choose_best_feature_to_split 方法 for value in unique_vals: sub_labels = labels[:] # 對符合該規則的特徵繼續生成子樹 my_tree[best_feat_label][value] = create_tree(split_data_set(data_set, best_feat, value), sub_labels) return my_tree def create_tree_run(): my_dat, labels = create_data_set() my_tree = create_tree(my_dat, labels) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() create_tree_run()
咱們已經手動爲咱們的流程生成了一個樹結構的字典,第一個關鍵字 ‘no surfacing’ 是第一個劃分數據集的特徵名字,該關鍵字的值也是另外一個數據字典。第二個關鍵字是 ‘no surfacing’ 特徵劃分的數據集,這些關鍵字的值是 ‘no surfacing’ 節點的子節點。若是值是類標籤,則蓋子節點是葉子節點;若是值是另外一個數據字典,則子節點是一個判斷節點,這種格式結構不斷重複就狗策很難過了整棵樹。
咱們已經用代碼實現了從數據集中建立樹,可是返回的結果是字典,並不容易讓人理解,可是決策樹的主要優勢是易於理解,所以咱們將使用 Matplotlib 庫建立樹形圖。
Matplotlib 庫提供了一個註解工具annotations,他能夠在數據圖形上添加文本註釋。而且工具內嵌支持帶箭頭的劃線工具。
所以咱們建立一個 tree_plotter.py 的文件經過註解和箭頭繪製樹形圖。
# tree_plotter.py import matplotlib.pyplot as plt from chinese_font import font # 定義文本框和箭頭格式 decision_node = dict(boxstyle='sawtooth', fc='0.8') leaf_node = dict(boxstyle='round4', fc='0.8') arrow_args = dict(arrowstyle='<-') def plot_node(node_txt, center_pt, parent_pt, node_type): """執行繪圖功能,設置樹節點的位置""" create_plot.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, fontproperties=font) def create_plot(): fig = plt.figure(1, facecolor='white') # 清空繪圖區 fig.clf() # 繪製兩個不一樣類型的樹節點 create_plot.ax1 = plt.subplot(111, frameon=False) plot_node('決策節點', (0.5, 0.1), (0.1, 0.5), decision_node) plot_node('葉節點', (0.8, 0.1), (0.3, 0.8), leaf_node) plt.show() if __name__ == '__main__': create_plot()
圖3-3 plot_node例子
咱們順利已經構造了一個能夠生成樹節點的方法,輸出結果如圖3-3 所示。
咱們已經能夠本身構造一個樹節點了,可是咱們的最終目標是構造一個樹來展現咱們的整個流程。構造樹不像構造樹節點同樣隨意,咱們須要知道樹有多少層,有多少個樹節點,每一個樹節點的位置。
所以咱們定義兩個新方法 get_num_leafs 和 get_tree_depth 來獲取葉節點的數目和樹的層度,而且爲了以後不用再去本身生成樹,咱們定義 retrieve_tree 方法生成樹。
# tree_plotter.py def get_num_leafs(my_tree): """獲取葉子節點總數""" # 取出根節點 num_leafs = 0 first_str = list(my_tree.keys())[0] # 循環判斷子節點 second_dict = my_tree[first_str] for key in second_dict.keys(): # 對於子節點爲判斷節點,繼續遞歸尋找葉子節點 if isinstance(second_dict[key], dict): num_leafs += get_num_leafs(second_dict[key]) else: num_leafs += 1 return num_leafs def get_tree_depth(my_tree): """獲取樹的層數""" # 找到根節點 max_depth = 0 first_str = list(my_tree.keys())[0] second_dict = my_tree[first_str] for key in second_dict.keys(): # 若是子節點爲判斷節點,繼續遞歸 if isinstance(second_dict[key], dict): this_depth = 1 + get_tree_depth(second_dict[key]) else: this_depth = 1 # 調用中間變量獲取樹的層數 if this_depth > max_depth: max_depth = this_depth return max_depth def retrieve_tree(i): list_of_trees = [{ 'no surfacing': { 0: 'no', 1: { 'flippers': { 0: 'no', 1: 'yes' } } } }, {'no surfacing': { 0: 'no', 1: { 'flippers': { 0: { 'head': { 0: 'no', 1: 'yes' } } }, 1: 'no', } } } ] return list_of_trees[i] def get_num_and_get_depth(): my_tree = retrieve_tree(0) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} num_leafs = get_num_leafs(my_tree) max_depth = get_tree_depth(my_tree) print(num_leafs) # 3 print(max_depth) # 2 if __name__ == '__main__': # create_plot() get_num_and_get_depth()
全部準備工做都已經齊全了,目前咱們只須要作的就是生成一顆完整的樹了。
讓咱們自定義 create_plot 方法來生成咱們的第一棵樹吧!
# treePlotter.py # TODO 補充解釋 def plot_mid_text(cntr_pt, parent_pt, txt_string): """在父子節點填充文本信息""" x_mid = (parent_pt[0] - cntr_pt[0]) / 2.0 + cntr_pt[0] y_mid = (parent_pt[1] - cntr_pt[1]) / 2.0 + cntr_pt[1] create_plot_2.ax1.text(x_mid, y_mid, txt_string) def plot_tree(my_tree, parent_pt, node_txt): """""" # 計算寬與高 num_leafs = get_num_leafs(my_tree) depth = get_tree_depth(my_tree) first_str = list(my_tree.keys())[0] cntr_pt = (plot_tree.xOff + (1.0 + float(num_leafs)) / 2.0 / plot_tree.total_w, plot_tree.yOff) # 標記子節點屬性值 plot_mid_text(cntr_pt, parent_pt, node_txt) plot_node(first_str, cntr_pt, parent_pt, decision_node) second_dict = my_tree[first_str] # 減小 y 偏移 plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.total_d for key in list(second_dict.keys()): if isinstance(second_dict[key], dict): plot_tree(second_dict[key], cntr_pt, str(key)) else: plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.total_w plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node) plot_mid_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key)) plot_tree.yOff = plot_tree.yOff + 1.0 / plot_tree.total_d def create_plot_2(in_tree): """""" # 建立並清空畫布 fig = plt.figure(1, facecolor='white') fig.clf() # axprops = dict(xticks=[], yticks=[]) create_plot_2.ax1 = plt.subplot(111, frameon=False, **axprops) # plot_tree.total_w = float(get_num_leafs(in_tree)) plot_tree.total_d = float(get_tree_depth(in_tree)) # plot_tree.xOff = -0.5 / plot_tree.total_w plot_tree.yOff = 1.0 plot_tree(in_tree, (0.5, 1.0), '') plt.show() def create_plot_2_run(): """create_plot_2的運行函數""" my_tree = retrieve_tree(0) create_plot_2(my_tree) if __name__ == '__main__': # create_plot() # get_num_and_get_depth() create_plot_2_run()
最終結果如圖3-4
圖3-4 超過兩個分支的樹
咱們已經學習瞭如何從原始數據中建立決策樹,而且可以使用 Python 繪製樹形圖,可是爲了咱們瞭解數據的真實含義,下面咱們將學習若是用決策樹執行數據分類。
# trees.py def classify(input_tree, feat_labels, test_vec): first_str = list(input_tree.keys())[0] second_dict = input_tree[first_str] feat_index = feat_labels.index(first_str) class_label = None for key in list(second_dict.keys()): if test_vec[feat_index] == key: if isinstance(second_dict[key], dict): class_label = classify(second_dict[key], feat_labels, test_vec) else: class_label = second_dict[key] return class_label def classify_run(): from 第三章.code.tree_plotter import retrieve_tree my_dat, labels = create_data_set() my_tree = retrieve_tree(0) class_label = classify(my_tree, labels, [1, 0]) print(class_label) # 'no'
index 函數解決在存儲帶有特徵的數據會面臨一個問題:程序沒法肯定特徵在數據集中的位置。
每次使用分類器時,都必須從新構造決策樹,而且構造決策樹是很耗時的任務。
所以咱們構造 store_tree 和 grab_tree 方法來建立好的決策樹。
# trees.py def store_tree(input_tree, filename): import pickle with open(filename, 'wb') as fw: pickle.dump(input_tree, fw) def grab_tree(filename): import pickle with open(filename, 'rb') as fr: return pickle.load(fr) def store_grab_tree_run(): import os from 第三章.code.tree_plotter import retrieve_tree my_tree = retrieve_tree(0) classifier_storage_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'classifierStorage.txt') store_tree(my_tree, classifier_storage_filename) my_tree = grab_tree(classifier_storage_filename) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() # create_tree_run() # classify_run() store_grab_tree_run()
決策樹經過一個小數據集技能學到不少知識。下面咱們經過一顆決策樹來幫助人們判斷須要佩戴的鏡片類型。
1. 收集數據: 提供的文本文件 2. 準備數據: 解析 tab 鍵分隔的數據行 3. 分析數據: 快速檢查數據,確保正確地解析數據內容,使用 create_plot 函數繪製最終的樹形圖。 4. 訓練算法: 使用以前的 create_tree 函數 5. 測試算法: 編寫測試函數驗證決策樹能夠正確分類給定的數據實例 6. 使用算法: 存儲輸的數據結構,以便下次使用時無需從新構造樹
加載源自 UCI 數據庫的隱形眼鏡數據集。
# trees.py def store_grab_tree_run(): import os from 第三章.code.tree_plotter import retrieve_tree my_tree = retrieve_tree(0) classifier_storage_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'classifierStorage.txt') store_tree(my_tree, classifier_storage_filename) my_tree = grab_tree(classifier_storage_filename) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} def create_lenses_tree(): import os lenses_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lenses.txt') with open(lenses_filename, 'rt', encoding='utf-8') as fr: lenses = [inst.strip().split('\t') for inst in fr.readlines()] lenses_labels = ['age', 'prescript', 'astigmatic', 'tearRate'] lenses_tree = create_tree(lenses, lenses_labels) return lenses_tree def plot_lenses_tree(): from 第三章.code import tree_plotter lenses_tree = create_lenses_tree() tree_plotter.create_plot(lenses_tree) if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() # create_tree_run() # classify_run() # store_grab_tree_run() plot_lenses_tree()
圖3-5 由 ID3 算法產生的決策樹
由圖3-4咱們能夠看出咱們只須要問4個問題就能肯定患者須要佩戴哪一種類型的眼鏡。
可是細心的同窗應該發現了咱們的決策樹很好的匹配了實驗數據,然而這些匹配選項可能太多了,咱們稱之爲過分匹配,爲了解決過分匹配的問題,咱們能夠裁剪決策樹,去掉不必的葉子節點。若是葉子節點只能增長少量信息,則能夠刪除該節點,並將它傳入到其餘葉子節點中。不過這個問題咱們須要之後使用決策樹構造算法 CART 來解決。而且若是存在太多的特徵劃分,ID3 算法仍然會面臨其餘問題。
決策樹分類器就像帶有終止塊的流程圖,終止塊表示分類結果。開始處理數據時,咱們首先須要測量集合中數據的不一致性,也就是熵,而後尋找最優方案劃分數據集,知道數據集中的全部數據屬於統一分類。
對數據進行分類後,咱們通常使用 Python 語言內嵌的數據結構字典存儲樹節點信息。
以後咱們使用 Matplotlib 的註解功能,咱們將存儲的樹結構轉化爲容易理解的圖像。可是隱形眼鏡的例子代表決策樹可能會產生過多的數據集,從而產生過分匹配數據集的問題。之後咱們能夠經過裁剪決策樹,合併相鄰的沒法產生大量信息增益的葉節點,消除過分匹配問題。