《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

玩世不恭的Coder 發表於 2020-11-21

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

在上篇文章中,《Machine Learning in Action》—— Taoye給你講講決策樹到底是支什麼“鬼”主要講述了決策樹的理論內容,介紹了什麼決策樹,以及生成決策樹時所需要優先選取的三種決策標準。有學習的過SVM,或閱讀過Taoye之前寫的幾篇SVM內容的文章可以發現,決策樹相對於SVM來講要簡單很多,沒有太多且複雜的公式推導。

我們在把之前的內容稍微回顧下:

  • 屬性特徵的資訊增益越高,按道理來講應當被優先選取,常用於\(ID3\)演算法
  • 屬性特徵的增益率越高,按道理來講應當被優先選取,常用與\(C4.5\)演算法
  • 屬性特徵的尼基指數低,按道理來講應當被優先選取,常用於\(CART\)演算法

有了前面的內容做基礎,閱讀本篇文章就會很輕鬆了。本篇主要講述以下幾部分的內容:

  • 基於ID3演算法手動構建決策樹,並通過Matplotlib進行視覺化
  • 基於已經構建好的決策樹進行分類預測
  • 構建好的決策樹模型應當如何儲存和讀取?
  • 通過鳶尾花(Iris)資料集,使用Sklearn構建決策樹,

一、基於ID3演算法手動構建決策樹,並通過Matplotlib進行視覺化

構建決策樹的演算法有好幾種,比如像ID3、C4.5、CART之類的,限於篇幅、時間和精力的關係,本篇文章主要採用ID3演算法來進行構建,使用到的決策標準(指標)是上篇文章中所提到的資訊增益。關於C4.5和CART演算法構建決策樹,有興趣的讀者可以參考上期中的增益率和尼基指數的內容。

本次構建決策樹所需要用到的資料集仍然是李航——《統計學習方法》中的貸款資料,這裡再次把資料集放出來瞅瞅:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

前面也有提到,ID3演算法主要是基於資訊增益作為選取屬性特徵的準則,在上期我們也計算過各個屬性特徵所對應的資訊增益值,如下:

\[\begin{aligned} & Gain(D, 年齡) = 0.083 \\ & Gain(D, 工作)=0.324 \\ & Gain(D, 房子)=0.420 \\ & Gain(D, 信用)=0.363 \end{aligned} \]

而根據ID3演算法過程,我們可以知道,需要優先選取資訊增益最大的屬性進行決策,即房子,也就是說將房子作為決策樹的根節點。由於房子這一個屬性特徵有兩個取值,所以引申出兩個子節點,一個對應“有房子”,另一個對應“無房子”,而“有房子”的六個樣本的類別都是允許放款,也就是有同一類樣本點,所以它理應成為一個葉子節點,且節點的類不妨標記為“允許”。

這樣一來,我們的根節點以及其中的一個葉子節點就確定了。接下來,我們需要將“無房子”所對應的樣本集再次選取一個新的屬性特徵進行決策。注意:此次做決策的資料樣本總體就不再是初始資料了,而是“無房子”所對應的所有樣本,這一點需要格外注意。

我們在“無房子的”的資料樣本中再次計算其他屬性所對應的資訊增益,我們不妨講此次的資料樣本集合記為\(D_1\),計算結果如下:

\[\begin{aligned} & Gain(D_1, 年齡) = 0.251 \\ & Gain(D_1, 工作)=0.918 \\ & Gain(D_1, 信用)=0.474 \end{aligned} \]

與上同樣分析,可以發現此時工作所對應的資訊增益值最高,也就是說第二個優先選取的屬性為“工作”。而且,我們可以發現,在資料樣本集\(D_2\)中,總共有9個,其中允許放款的有三個,拒絕的有6個,且結果標籤與工作值剛好完全都對應上了,也就是說有工作的都允許放款了,沒工作的都拒絕放款了。所以,在第二個屬性特徵選取完成之後,此時產生了倆個葉子節點,節點結果與是否有工作對應。

通過如上分析,我們就得到了此次基於ID3演算法所構建出的決策樹,決策樹如下:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

接下來我們通過程式碼來生成這顆決策樹,對於樹形結構的資料,我們可以通過字典或者說是json型別來進行儲存。比如上圖中的決策樹,我們可以通過如下結果來進行表示:

{"房子": {
    "1": "Y",
    "0":{"工作": {
        "1": "Y",
        "0": "N"
    }}
}}

上述表示的資料格式我們一般稱其為Json,這個在前後端、爬蟲,亦或是在其他各種領域中都是接觸的非常多的。另外,我們可以發現在決策樹生成的過程中,在一個屬性特徵選取完成之後,需要經過同樣的操作再次選取一個屬性特徵,其實就相當於一個週期,換句話講正好滿足了遞迴的特性,只是我們的資料總體發生了變化而已。既然我們明確了儲存樹形結構資料所需要的型別,下面我們通過程式碼來實現:

此次的程式碼相較於上篇文章中計算資訊增益的變化主要有三個地方:

  • 由於我們最終需要生成屬性的具體內容,而非僅僅索引,所以我們在建立資料的時候,除了返回資料本身之外,還需要返回對應的屬性,修改establish_data方法如下所示:
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立訓資料集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本資料集相關資訊,前面幾項代表屬性特徵,最後一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀", "工作", "房子", "信用"]
    return np.array(data), labels
  • 在上篇文章中,我們的handle_data方法僅僅是找出對應屬性特徵值的樣本,比如找出所有年紀為青年的樣本資料集。而要想構建決策樹,在第一次選取了屬性之後,應當將該屬性從資料集中移除,所以修改handle_data如下:
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應屬性特徵值的樣本,比如找出所有年紀為青年的樣本資料集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data
  • 第三個就是需要一個建立決策樹的方法establish_decision_tree,具體程式碼如下,其中在選取完成一個屬性之後需要遞迴呼叫本身來選取第二個屬性
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 資料集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過資訊增益優先選取最好的屬性特徵
    best_label = labels[best_feature_index]   # 屬性特徵對應的標籤內容
    # feat_labels表示已選取的屬性;新建一個決策樹節點;將屬性標籤列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優屬性對應值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

該部分的完整程式碼如下所示:

import numpy as np
import pandas as pd

np.__version__
pd.__version__

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立訓資料集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本資料集相關資訊,前面幾項代表屬性特徵,最後一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算資訊熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應屬性特徵值的樣本,比如找出所有年紀為青年的樣本資料集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算最大的資訊增益,並輸出其所對應的特徵索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 屬性特徵的數量
    base_entropy = calc_information_entropy(data)                 # 計算總體資料集的資訊熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大資訊增益和對應的特徵索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 計算屬性特徵劃分後的資訊增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 計運算元集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 計算資訊增益
        print("第%d個屬性特徵所對應的的增益為%.3f" % (index + 1, temp_information_gain))            # 輸出每個特徵的資訊增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新資訊增益,確定的最大的資訊增益對應的索引
    return best_feature

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 資料集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過資訊增益優先選取最好的屬性特徵
    best_label = labels[best_feature_index]   # 屬性特徵對應的標籤內容
    # feat_labels表示已選取的屬性;新建一個決策樹節點;將屬性標籤列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優屬性對應值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

if __name__ == "__main__":
    data, labels = establish_data()
    print(establish_decision_tree(data, labels, list()))

執行結果:

{'房子': {'1': 'Y', '0': {'工作': {'1': 'Y', '0': 'N'}}}}

可見,程式碼執行的結果與我們手動建立的決策樹如出一轍,完美,哈哈哈~~~

可是,如上決策樹的顯示未免有點太不親民了,生成的決策樹比較簡單那還好點,假如我們生成的決策樹比較複雜,那通過Json格式的資料來輸出決策樹就有點懵了。

對此,我們需要將決策樹進行視覺化,視覺化主要使用到了Matplotlib包。關於Matplotlib的使用大家可以參考文件及其他資料,Taoye後期也會整理出一篇自己常用的介面。

import numpy as np
import pandas as pd

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立訓資料集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 樣本資料集相關資訊,前面幾項代表屬性特徵,最後一項表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年紀", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算資訊熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:找出對應屬性特徵值的樣本,比如找出所有年紀為青年的樣本資料集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:計算最大的資訊增益,並輸出其所對應的特徵索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 屬性特徵的數量
    base_entropy = calc_information_entropy(data)                 # 計算總體資料集的資訊熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大資訊增益和對應的特徵索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 計算屬性特徵劃分後的資訊增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 計運算元集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 計算資訊增益
        print("第%d個屬性特徵所對應的的增益為%.3f" % (index + 1, temp_information_gain))            # 輸出每個特徵的資訊增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新資訊增益,確定的最大的資訊增益對應的索引
    return best_feature

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立決策樹
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 資料集中的類別只有一種
    best_feature_index = calc_information_gain(data)    # 通過資訊增益優先選取最好的屬性特徵
    best_label = labels[best_feature_index]   # 屬性特徵對應的標籤內容
    # feat_labels表示已選取的屬性;新建一個決策樹節點;將屬性標籤列表中刪除已選取的屬性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 獲取最優屬性對應值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:統計決策樹當中的葉子節點數目,以及決策樹的深度
"""
def get_leaf_number_and_tree_depth(decision_tree):
    leaf_number, first_key, tree_depth = 0, next(iter(decision_tree)), 0; second_dict = decision_tree[first_key]
    for key in second_dict.keys():
        if type(second_dict.get(key)).__name__ == "dict":
            temp_number, temp_depth = get_leaf_number_and_tree_depth(second_dict[key])
            leaf_number, curr_depth = leaf_number + temp_number, 1 + temp_depth
        else: leaf_number += 1; curr_depth = 1
        if curr_depth > tree_depth: tree_depth = curr_depth
    return leaf_number, tree_depth

from matplotlib.font_manager import FontProperties

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:繪製節點
"""
def plot_node(node_text, center_pt, parent_pt, node_type):
    arrow_args = dict(arrowstyle = "<-")
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 設定字型
    create_plot.ax1.annotate(node_text, xy=parent_pt,  xycoords='axes fraction',
                            xytext=center_pt, textcoords='axes fraction',
                            va="center", ha="center", bbox=node_type, arrowprops=arrow_args, FontProperties=font)

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:標註有向邊的值
"""
def tag_text(cntr_pt, parent_pt, node_text):
    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.ax1.text(x_mid, y_mid, node_text, va="center", ha="center", rotation=30)
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:繪製決策樹
"""
def plot_tree(decision_tree, parent_pt, node_text):
    decision_node = dict(boxstyle="sawtooth", fc="0.8")
    leaf_node = dict(boxstyle = "round4", fc = "0.8")
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(decision_tree)
    first_key = next(iter(decision_tree))
    cntr_pt = (plot_tree.xOff + (1.0 + float(leaf_number)) / 2.0 / plot_tree.totalW, plot_tree.yOff)
    tag_text(cntr_pt, parent_pt, node_text); plot_node(first_key, cntr_pt, parent_pt, decision_node)
    second_dict = decision_tree[first_key]
    plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.totalD
    for key in second_dict.keys():
        if type(second_dict[key]).__name__ == 'dict': plot_tree(second_dict[key], cntr_pt, str(key))
        else:
            plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.totalW
            plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node)
            tag_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key))
    plot_tree.yOff = plot_tree.yOff + 1.0 / plot_tree.totalD

from matplotlib import pyplot as plt
"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:建立決策樹
"""
def create_plot(in_tree):
    fig = plt.figure(1, facecolor = "white")
    fig.clf()
    axprops = dict(xticks = [], yticks = [])
    create_plot.ax1 = plt.subplot(111, frameon = False, **axprops)
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(in_tree)
    plot_tree.totalW, plot_tree.totalD = float(leaf_number), float(tree_depth)
    plot_tree.xOff = -0.5 / plot_tree.totalW; plot_tree.yOff = 1.0
    plot_tree(in_tree, (0.5,1.0), '')
    plt.show()
    
if __name__ == "__main__":
    data, labels = establish_data()
    decision_tree = establish_decision_tree(data, labels, list())
    print(decision_tree)
    print("決策樹的葉子節點數和深度:", get_leaf_number_and_tree_depth(decision_tree))
    create_plot(decision_tree)

手動視覺化決策樹的結果如下所示:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

實話實說,通過Matplotlib手動對決策樹進行視覺化,對於之前沒什麼經驗的碼手來講確實有點不友好。上述程式碼能看懂就行,沒必要死揪著不放,後面的話會介紹通過Graphviz來繪製決策樹。這裡對上方的程式碼做個簡短的說明:

  • get_leaf_number_and_tree_depth主要用於統計決策樹當中的葉子節點數目,以及決策樹的深度。選取key對應的value,判斷value是否為一個字典型別,否的話說明是一個葉子節點,是的話說明非葉子節點,不同情況做不同處理
  • plot_node方法用於繪製節點,這裡設定了font型別是在windows下的,如果是linux則需要額外設定
  • tag_text用於標註有向邊的屬性值,在這裡主要是用1和0來進行標註,1代表對屬性的肯定,0表示否定
  • plot_tree遍歷繪製決策樹,在這裡需要呼叫前面所定義的幾個方法

二、基於已經構建好的決策樹進行分類預測

依靠訓練資料構造了決策樹之後,我們既可以通過該決策樹模型應用於實際資料來進行分類。在對資料進行分類時,需要使用決策樹以及用於構造決策樹的標籤向量;然後,程式比較測試資料與決策樹上的數值,遞迴執行該過程直到進入到葉子節點;最後將測試資料定義為葉子節點所屬的型別。——《機器學習實戰》

在已經獲取到決策樹模型的前提下對測試資料進行分類還是挺好理解的

對此,我們定義一個classify方法來進行分類:

"""
    Author: Taoye
    微信公眾號: 玩世不恭的Coder
    Explain:通過決策樹模型對測試資料進行分類
""" 
def classify(decision_tree, best_feature_labels, test_data):
    first_node = next(iter(decision_tree))
    second_dict = decision_tree[first_node]
    feat_index = best_feature_labels.index(first_node)
    for key in second_dict.keys():
        if int(test_data[feat_index]) == int(key):
            if type(second_dict[key]).__name__ == "dict":   # 為字典說明還沒到葉子節點
                result_label = classify(second_dict[key], best_feature_labels, test_data)
            else: result_label = second_dict[key]
    return result_label

我們分別對四個資料樣本進行測試,樣本分別是(有房子,沒工作),(有房子,有工作),(沒房子,沒工作),(沒房子,有工作),使用列表表示分別是[1, 0], [1, 1], [0, 0], [0, 1],執行結果如下:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

可見,四組資料都能夠分類成功。

三、構建好的決策樹模型應當如何儲存和讀取?

構建好決策樹之後,我們要想儲存該模型就可以通過pickle模組來進行。

儲存好模型之後,下次使用該模型就不需要的再次訓練了,只需要載入模型即可。儲存和載入模型的示例程式碼如下(挺簡單的,就不多說了):

import pickle
with open("DecisionTreeModel.txt", "wb") as f:
    pickle.dump(decision_tree, f)           # 儲存決策樹模型

f = open("DecisionTreeModel.txt", "rb")
decision_tree = pickle.load(f)             # 載入決策樹模型

四、通過鳶尾花(Iris)資料集,使用Sklearn構建決策樹

現在我們通過sklearn來實現一個小案例,資料集採用的是機器學習中比較常用的鳶尾花(Iris)資料集。更多其他關於決策樹分類的案例,大家可以去sklearn.tree.DecisionTreeClassifier的文件中學習:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

在sklearn中實現決策樹分類主要用到的介面是sklearn.tree.DecisionTreeClassifier,這個主要是通過資料樣本集構建一個決策樹模型。此外,如果我們要想將決策樹視覺化,還需要使用到export_graphviz。當然了,在sklearn.tree`下還有其他介面可供大家呼叫,這裡不做的過多介紹了,讀者可自行學習。

關於sklearn.tree.DecisionTreeClassifier的使用,可參考:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html。其中內建了很多的引數,這裡主要記錄8個引數,也方便後期回顧,其他的有機會用到再查詢資料:

  • criterion:屬性選取的標準,預設採用的是gini,也可以自行選擇entropygini是基尼值,entropy是資訊熵,這兩個我們在上篇文章中已經講到過了
  • splitter:特徵劃分節點的選擇標準,預設是best,可以設定為random。預設的"best"適合樣本量不大的時候,而如果樣本資料量非常大,此時決策樹構建推薦"random"。
  • max_depth:決策樹最大深度,預設是None。需要注意一點的是,該深度是不包含根節點的。一般來說,資料少或者特徵少的時候可以不管這個值。如果模型樣本量多,特徵也多的情況下,推薦限制這個最大深度,具體的取值取決於資料的分佈。
  • max_features:劃分時考慮的最大特徵數,預設是None。一般來說,如果樣本特徵數不多,比如小於50,我們用預設的"None"就可以了,如果特徵數非常多,我們可以靈活使用其他取值來控制劃分時考慮的最大特徵數,以控制決策樹的生成時間。用到的時候檢視下文件即可。
  • min_samples_split:內部節點再劃分所需最小樣本數,預設為2。意思就是說,比如我們某個屬性對應樣本數目小於min_samples_split,即使它滿足優先選取條件,依然會被剔除掉。
  • min_samples_leaf:葉子節點最少樣本數,預設是1。這個值限制了葉子節點最少的樣本數,如果某葉子節點數目小於樣本數,則會和兄弟節點一起被剪枝。葉結點需要最少的樣本數,也就是最後到葉結點,需要多少個樣本才能算一個葉結點。如果設定為1,哪怕這個類別只有1個樣本,決策樹也會構建出來。
  • max_leaf_nodes:最大葉子節點數,預設是None。通過限制最大葉子節點數,可以防止過擬合。如果加了限制,演算法會建立在最大葉子節點數內最優的決策樹。如果特徵不多,可以不考慮這個值,但是如果特徵分成多的話,可以加以限制,具體的值可以通過交叉驗證得到。
  • random_state:隨機數種子,預設是None。如果沒有設定隨機數,隨機出來的數與當前系統時間有關,每個時刻都是不同的。如果設定了隨機數種子,那麼相同隨機數種子,不同時刻產生的隨機數也是相同的。

此外,在DecisionTreeClassifier下也有很多方法可供呼叫,詳情可參考文件進行使用,如下:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

接下來,我們就用sklearn來對鳶尾花資料集進行分類吧。參考資料:https://scikit-learn.org/stable/auto_examples/tree/plot_iris_dtc.html#sphx-glr-auto-examples-tree-plot-iris-dtc-py

構建決策樹本身的程式碼並不難,主要在於視覺化,其中涉及到了Matplotlib的不少操作,以增強視覺化效果,完整程式碼如下:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree

class IrisDecisionTree:
    """
        Explain:屬性的初始化
        Parameters:
            n_classes: 鳶尾花的類別數
            plot_colors: 不同類別花的顏色
            plot_step: meshgrid網格的步長
    """
    def __init__(self, n_classes, plot_colors, plot_step):
        self.n_classes = n_classes
        self.plot_colors = plot_colors
        self.plot_step = plot_step
    
    """
        Explain: 通過load_iris構建資料集
    """
    def establish_data(self):
        iris_info = load_iris()
        return iris_info.data, iris_info.target, iris_info.feature_names, iris_info.target_names
    
    """
        Explain:分類的視覺化
    """
    def show_result(self, x_data, y_label, feature_names, target_names):
        # 選取兩個屬性來構建決策樹,以方便視覺化,其中列表內部元素代表屬性對應的索引
        for index, pair in enumerate([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]):
            sub_x_data, sub_y_label = x_data[:, pair], y_label
            clf = DecisionTreeClassifier().fit(sub_x_data, sub_y_label)   # 選取兩個屬性構建決策樹
            plt.subplot(2, 3, index + 1)
            x_min, x_max = sub_x_data[:, 0].min() - 1, sub_x_data[:, 0].max() + 1   # 第一個屬性
            y_min, y_max = sub_x_data[:, 1].min() - 1, sub_x_data[:, 1].max() + 1   # 第二個屬性
            xx, yy = np.meshgrid(np.arange(x_min, x_max, self.plot_step), np.arange(y_min, y_max, self.plot_step))
            plt.tight_layout(h_pad=0.5, w_pad=0.5, pad=2.5)
            Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)    # 預測meshgrid內部每個元素的分類
            cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)   # 繪製帶有顏色的網格圖
            plt.xlabel(feature_names[pair[0]]); plt.ylabel(feature_names[pair[1]])     # 標註座標軸標籤
            for i, color in zip(range(self.n_classes), self.plot_colors):
                idx = np.where(sub_y_label == i)
                plt.scatter(sub_x_data[idx, 0], sub_x_data[idx, 1], c=color, label=target_names[i],
                            cmap=plt.cm.RdYlBu, edgecolor='black', s=15)    # 繪製資料樣本集的散點圖
        
        from matplotlib.font_manager import FontProperties
        font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 定義中文字型
        plt.suptitle("通過決策樹對鳶尾花資料進行視覺化", fontproperties=font)
        plt.legend(loc='lower right', borderpad=0, handletextpad=0)
        plt.axis("tight")
        plt.figure()
        clf = DecisionTreeClassifier().fit(x_data, y_label)     # 針對鳶尾花資料集的多重屬性來構建決策樹
        plot_tree(clf, filled=True)
        plt.show()
    
if __name__ == "__main__":
    iris_decision_tree = IrisDecisionTree(3, "ryb", 0.02)
    x_data, y_label, feature_names, target_names = iris_decision_tree.establish_data()
    iris_decision_tree.show_result(x_data, y_label, feature_names, target_names)

執行結果如下所示:

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

通過視覺化結果,我們可以發現主要有兩個結果,我們分別對其進行說明下:於鳶尾花資料集來講,總共有四種屬性特徵以及三種標籤結果。為了方便視覺化,第一張圖只選取了兩個屬性來構建決策樹,四種屬性,選兩個,很簡單,學過排列組合的都應該知道有\(C_4^2=6\)種可能,所第一張圖中的每張子圖分別對應一種可能。且顏色不同代表不同的分類,假如資料集顏色與網格內部顏色一致,則說明分類正確。因此,從直觀上來看,選取sepal length和petal length這兩種屬性構建的決策樹相對較好。而第二張圖是針對資料集中的所有屬性特徵來構建決策樹。具體的結果可自行執行上方程式碼檢視(由於設定了font字型,所以上方程式碼需在windows下執行)

我們可以發現,上面程式碼視覺化決策樹的時候採用的是sklearn.tree.plot_tree,前面我們在講解通過Matplotlib繪製決策樹的時候也有說到,使用graphviz亦可視覺化決策樹,下面我們不妨來看看吧!

graphviz不能採用pip進行安裝,採用anaconda安裝的時候也會很慢,甚至多次嘗試都可能安裝失敗,前幾天幫同學安裝就出現這種情況(windows下是這樣的,linux環境下會很方便),所以這裡我們採用直接通過whl檔案來安裝。

建議:對於使用Python有過一段時間的Pyer來講,都會經常安裝一些第三方模組,有些可以直接通過pip或者conda完美的解決,而有些在安裝的過程中會遇到各種不明所以的錯誤。所以,對於在安裝過程中遇到錯誤的讀者不妨嘗試通過whl檔案進行安裝,whl目標地址:https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud,其中整合了各種Python模組的whl檔案。

開啟上述url地址 --> ctrl + f搜尋graphviz --> 下載需要的graphviz安裝檔案

《Machine Learning in Action》—— hao朋友,快來玩啊,決策樹呦

在本地目標路徑中執行安裝即可:

pip install graphviz‑0.15‑py3‑none‑any.whl

此外對於windows來將 ,還需要前往官網安裝graphviz的exe檔案,然後將bin目錄新增到環境變數即可。exe檔案的下載地址:https://graphviz.org/download/

如果是Linux使用者,那就比較方便了,直接通過命令安裝即可:

$ sudo apt install graphviz         # Ubuntu
$ sudo apt install graphviz         # Debian

至此,Graphviz就已經安裝好了。我們通過它來實現決策樹的視覺化吧,在IrisDecisionTree下新增如下show_result_by_graphviz方法:

"""
    Explain:通過graphviz實現決策樹的視覺化
"""
def show_result_by_graphviz(self, x_data, y_label):
    clf = DecisionTreeClassifier().fit(x_data, y_label)
    iris_dot_data = tree.export_graphviz(clf, out_file=None, 
                                         feature_names=iris.feature_names,  
                                         class_names=iris.target_names,  
                                         filled=True, rounded=True,  
                                         special_characters=True) 
    import graphviz
    graph = graphviz.Source(iris_dot_data); graph.render("iris")

執行之後會在當前目錄下生成一個pdf檔案,其中就是視覺化之後的決策樹。注意:以上只是實現簡單的鳶尾花的決策樹分類,讀者可通過調解DecisionTreeClassifier的引數構建不同的決策樹,以此來判別各個決策樹的優劣。

以上就是本篇全部內容了,決策樹的相關內容暫時就更新到這了,其他內容像過擬合、剪枝等等以後有時間再更新,下期的機器學習系列文章就是肝SVM的非線性模型了。

就不嘮嗑了~~~

我是Taoye,愛專研,愛分享,熱衷於各種技術,學習之餘喜歡下象棋、聽音樂、聊動漫,希望藉此一畝三分地記錄自己的成長過程以及生活點滴,也希望能結實更多志同道合的圈內朋友,更多內容歡迎來訪微信公主號:玩世不恭的Coder

參考資料:

[1] 《機器學習實戰》:Peter Harrington 人民郵電出版社
[2] 《統計學習方法》:李航 第二版 清華大學出版社
[3] 《機器學習》:周志華 清華大學出版社
[4] Python Extension Packages:https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud
[5] sklearn.tree.DecisionTreeClassifier:https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
[6] Graphviz官網:https://graphviz.org/

推薦閱讀

《Machine Learning in Action》—— Taoye給你講講決策樹到底是支什麼“鬼”
《Machine Learning in Action》—— 剖析支援向量機,優化SMO
《Machine Learning in Action》—— 剖析支援向量機,單手狂撕線性SVM
print( "Hello,NumPy!" )
幹啥啥不行,吃飯第一名
Taoye滲透到一家黑平臺總部,背後的真相細思極恐
《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?
那些年,我們玩過的Git,真香
基於Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度學習環境
網路爬蟲之頁面花式解析
手握手帶你瞭解Docker容器技術