機器學習系列 06:決策樹 01

  本內容將介紹用於分類的決策樹(decision tree),以及 ID3、C4.5 和 CART 算法。

  決策樹(decision tree)是一種基本的分類與迴歸方法。決策樹模型呈樹形結構,分爲分類樹、迴歸樹和模型樹,前者用於分類,後者用於預測實數值。其主要優點是模型具有可讀性,分類速度快。決策樹學習通常包括 3 個步驟:特徵選擇、決策樹的生成和決策樹的剪枝。這些決策樹學習的思想主要來源於由 Quinlan 在 1986 年提出的 ID3 算法和 1993 年提出的 C4.5 算法,以及由 Breiman 等人在 1984 年提出的 CART 算法。

  本內容主要介紹分類決策樹,不涉及迴歸樹和模型樹。可以參閱 機器學習系列:決策樹 02 - CART 算法 瞭解 CART 算法和迴歸樹、模型樹。

一、決策樹模型和學習

1.1 決策樹模型

  一棵決策樹包含一個根節點、若干個內部節點和若干個葉節點。圖-1 爲一棵決策樹,橢圓形爲判斷模塊,進行特徵判斷;矩形爲終止模塊,進行結果決策。從根節點到每個葉節點的路徑對應了一個判斷測試序列。

  用決策樹進行預測,從根節點開始,對實例的某一特徵進行判斷,根據判斷結果,將實例分配到其子節點。如此遞歸地對實例進行判斷並分配,直至到達葉節點。最後根據葉節點對實例進行決策,分類樹輸出類別,迴歸樹輸出預測值。

在這裏插入圖片描述
                        圖-1 決策樹模型

1.2 決策樹學習

  決策樹的學習,也稱爲決策樹的生成或決策樹的構造。決策樹學習過程:得到原始數據集,選擇最優劃分特徵,並根據此特徵劃分數據集,數據將被向下傳遞到子節點。如果子節點上的數據滿足特定條件,將構建葉節點;否則,再次選擇最優特徵劃分數據集。如此遞歸地劃分數據集,最終將形成一棵決策樹。決策樹的生成就是遞歸地構建樹的過程。

  以分類樹進行說明,上面提到的特定條件爲:

  • 當前節點包含的樣本全屬於同一類別,無需劃分。
  • 當前可劃分屬性集爲空,或是所有樣本在所有屬性上取值相同,無法劃分。
  • 當前節點包含的樣本集合爲空,不能劃分。

  對上面提到的情況,構造葉節點的類別設定方法:

  • 設定爲該節點所含樣本的類別(所有樣本爲同一類別)。
  • 設定爲該節點所含樣本最多的類別(即採用多數表決法決定葉節點的類別)。
  • 設定爲其父節點所含樣本最多的類別。

  決策樹學習的目的是爲了產生一棵泛化能力強,即處理未見實例能力強的決策樹。其基本流程如下:

  

函數 create_tree() 的僞代碼大致如下:
create_tree():
    if 數據集滿足特定條件:
        將該節點存爲葉節點
        return
    end if
    
    根據最優特徵劃分數據集,產生多個子集
    for 每個劃分的子集:
        if 子集爲空:
            構造葉結點,其類別爲父節點所含樣本最多的類別
        else:
            調用函數 create_tree() 遞歸劃分子集
        end if
    end for

  決策樹是一種貪心算法,它要在給定時間內做出最佳選擇,但並不關心能否達到全局最優。

  接下來我們需要了解如何選擇最優劃分特徵,以及如何劃分數據集。

二、特徵選擇

  特徵選擇在於選取對訓練數據具有分類能力的特徵。這樣可以提高決策樹學習的效率。通常特徵選擇的準則有信息增益、信息增益比和基尼指數等。在具體介紹它們之前,我們先來了解一下熵和條件熵。

2.1 熵與條件熵

2.1.1 熵

  在信息論與概率統計中,(entropy,又被稱爲信息熵)是表示隨機變量不確定性的度量。假設 XX 是一個取有限個值的離散隨機變量,其概率分佈爲

P(X=xi)=pi,i=1,2, ,n P(X=x_i) = p_i,\quad i=1,2,\cdots,n

則隨機變量 XX 的熵定義爲

(1)H(X)=i=1npilogpi H(X) = -\sum_{i=1}^{n} p_i logp_i \tag{1}

約定若 pi=0p_i = 0 時,pilogpi=0p_ilogp_i = 0H(X)H(X) 的最小值爲 0,最大值爲 lognlogn。通常,式(1)中的對數以 2 、自然常數或 10 爲底,熵的單位分別稱爲比特(bit)、納特(nat)或 Hart。熵越大,隨機變量的不確定性就越大。

2.1.2 條件熵

  假設隨機變量 (X,Y)(X, Y) 的聯合概率分佈爲

P(X=xi,Y=yj)=pij,i=1,2, ,n;j=1,2, ,m P(X=x_i, Y=y_j)=p_{ij}, \quad i=1,2,\cdots,n;j=1,2,\cdots,m

  條件熵 H(YX)H(Y|X) 表示在已知隨機變量 XX 的條件下隨機變量 YY 的不確定性。隨機變量 XX 給定條件下 YY 的條件熵(conditional entropy)H(YX)H(Y|X),定義爲 XX 給定條件下 YY 的條件概率分佈的熵對 XX 的數學期望

(2)H(YX)=i=1npiH(YX=xi),pi=P(X=xi);i=1,2, ,n H(Y|X) = \sum_{i=1}^n p_i H(Y|X=x_i), \quad p_i = P(X=x_i); i=1,2,\cdots,n \tag{2}

  當熵和條件熵中的概率由數據估計(特別是極大似然估計)得到時,所對應的熵和條件熵分別稱爲經驗熵(empirical entropy)和經驗條件熵(empirical conditional entropy)。

2.2 信息增益

  信息增益(information gain)表示得知特徵 XX 的信息而使得類 YY 的信息的不確定性減少的程度。

  特徵 AA 對訓練數據集 DD 的信息增益 Gain(D,A)Gain(D,A),定義爲集合 DD 的經驗熵 H(D)H(D) 與特徵 AA 給定條件下 DD 的經驗條件熵 H(DA)H(D|A) 之差,即

(3)Gain(D,A)=H(D)H(DA) Gain(D,A) = H(D) - H(D|A) \tag{3}

  假設訓練數據集爲 DD,存在以下設定:

  • D|D| 爲其樣本容量,即樣本個數。
  • 訓練數據集 DDKK 個類 CkC_kk=1,2, ,Kk=1,2,\cdots,KCk|C_k| 爲屬於類 CkC_k 的樣本個數。
  • 特徵 AAnn 個不同的取值 {a1,a2, ,an}\{a_1,a_2,\cdots,a_n\},根據特徵 AA 的取值將 DD 劃分爲 nn 個子集 D1,D2, ,DnD_1,D_2,\cdots,D_nDi|D_i|DiD_i 的樣本個數。
  • 子集 DiD_i 中屬於類 CkC_k 的樣本的集合爲 DikD_{ik}Dik|D_{ik}|DikD_{ik} 的樣本個數。

  則使用特徵 AA 劃分訓練數據集 DD 獲得的信息增益爲

(4)Gain(D,A)=(k=1KCkDlogCkD)(i=1nDiDH(Di)) Gain(D,A) = \Big(-\sum_{k=1}^{K} \frac{|C_k|}{|D|}log\frac{|C_k|}{|D|}\Big) - \Big(-\sum_{i=1}^{n} \frac{|D_i|}{|D|}H(D_i) \Big) \tag{4}

其中,H(Di)H(D_i)

(5)H(Di)=k=1KDikDilogDikDi H(D_i) = -\sum_{k=1}^{K} \frac{|D_{ik}|}{|D_i|} log\frac{|D_{ik}|}{|D_i|} \tag{5}

  ID3 算法就是使用信息增益來選擇劃分特徵。

2.3 信息增益率

  信息增益準則對可取值數目較多的特徵有所偏好,爲減少這種偏好可能帶來的不利影響,可使用信息增益率(information gain ratio)來選擇最優劃分特徵。

(6)Gain_ratio(D,A)=Gain(D,A)IV(A) Gain\_ratio(D,A) = \frac{Gain(D,A)}{IV(A)} \tag{6}

其中,IV(A)IV(A)

(7)IV(A)=i=1nDiDlogDiD IV(A) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|} log\frac{|D_i|}{|D|} \tag{7}

稱爲特徵 AA 的“固有值”(intrinsic value)。特徵 AA 的可能取值數目越多(即 nn 越大),則 IV(A)IV(A) 的值通常會越大。

   C4.5算法就是使用信息增益率來選擇劃分特徵。 需注意的是,增益率準則對可取值數目較少的特徵有所偏好。因此,C4.5 算法並不是直接選擇增益率最大的候選劃分特徵,而是使用了一個啓發式:先從候選劃分特徵中找出信息增益高於平均水平的特徵,再從中選擇增益率最高的。

2.4 基尼指數

  基尼指數(Gini index)定義爲

(8)Gini(D)=k=1Kkkpkpk Gini(D) = \sum_{k=1}^{K} \sum_{k^{'} \neq k}p_kp_{k^{'}} \tag{8}

(9)=k=1Kpk(1pk) = \sum_{k=1}^{K} p_k(1-p_k) \tag{9}

(10)=1k=1Kpk2 = 1 - \sum_{k=1}^{K}p_k^2 \tag{10}

  從上面的公式可知,基尼指數有兩種不同理解形式:

  • 從式(8)來看,Gini(D)Gini(D) 表示從數據集 DD 中隨機抽取兩個樣本,其類別標記不一致的概率。
  • 從式(9)來看,Gini(D)Gini(D) 表示從數據集 DD 中隨機選中的一個樣本被分錯的概率。

  因此,Gini(D)Gini(D)​ 越小,則數據集 DD​ 的純度越高。

  則使用特徵 AA 劃分訓練數據集後的基尼指數爲

(11)Gini(D,A)=i=1nDiDGini(Di) Gini(D,A) = \sum_{i=1}^{n} \frac{|D_i|}{|D|} Gini(D_i) \tag{11}

其中 Gini(Di)Gini(D_i)

(12)Gini(Di)=1k=1K(DikDi)2 Gini(D_i) = 1 - \sum_{k=1}^{K} \Big(\frac{|D_{ik}|}{|D_i|}\Big)^2 \tag{12}

  CART 算法進行分類時,就是使用基尼指數來選擇劃分特徵。

三、劃分數據集方法

3.1 離散屬性

  針對離散屬性,可以採用多分法或者二分法劃分數據集。假設有數據集 DD,其中屬性 AAnn 個不同的取值,根據屬性 AA 的取值將數據集劃分爲 nn 個子集,就是多分法;選擇其中的某個取值 aia_i,根據該值將數據集劃分爲 2 個子集 D1D_1D2D_2D1D_1 包含屬性 AA 不大於 aia_i 的樣本,D2D_2 包含屬性 AA 大於 aia_i 的樣本,就是二分法。

  注意:如果採用多分法後,該屬性不能作爲其後代節點的劃分屬性;如果採用二分法,該屬性可以繼續作爲其後代節點的劃分屬性。

3.2 連續屬性

  由於連續性屬性的可取值不再有限,因此,不能直接根據連續型屬性的可取值來對節點進行劃分。此時,連續型屬性離散化技術可派上用場。最簡單的策略是採用二分法(bi-partition)對連續屬性進行處理,C4.5 和 CART 決策樹算法都使用該方法。

  注意:因爲連續屬性只能採用二分法,所以該屬性還可作爲其後代節點的劃分屬性。

  ID3 算法不能處理連續屬性,C4.5 和 CART 算法可以處理連續型屬性。

四、決策樹的剪枝

  在決策樹學習中,爲了儘可能對數據集進行正確分類,從而構建出過於複雜的決策樹。這樣產生的樹往往對訓練數據集的分類很正確,但對未知的測試數據的分類卻沒有那麼準確,即出現過擬合現象。可以通過降低對決策樹的複雜度(即去掉一些分支)來解決這個問題,這個過程稱之爲剪枝(pruning)。

  決策樹剪枝有預剪枝(prepruning)和後剪枝(postpruning)兩種。預剪枝是指在決策樹生成過程中,對每個節點在劃分前後進行估計,若當前節點的劃分不能帶來決策樹泛化性能提升,則停止劃分並將當前節點標記爲葉節點。後剪枝則是先從訓練集生成一棵完整的決策樹;然後從上到下找到葉節點,用測試集來判斷將這些葉節點合併是否能降低測試誤差,如果能就進行合併。

  決策樹的生成對應於模型的局部選擇,決策樹的剪枝對應於模型的全局選擇。決策樹的生成只考慮局部最優,相對地,決策樹的剪枝則考慮全局最優。

  在 機器學習系列:決策樹 02 - CART 算法 裏面會講到剪枝的具體實現。

五、代碼實現

  在生成分類決策樹時,ID3、C4.5 和 CART 三種算法的流程基本相同,基本上只需要在特徵選擇和特徵劃分時進行不同處理即可。下面給出 ID3 算法的 Python 代碼實現(Python 3.x):

from math import log


class DecisionTreeID3:
    def __init__(self):
        pass

    def creat_tree(self, data_set, feature_labels):
        """
        創建決策樹

        data_set:創建決策樹的數據集
        feature_labels:特徵名列表
        """
        labels_list = [sample[-1] for sample in data_set]
        # 數據集中的樣本屬於同一個類別,返回這個類別
        if labels_list.count(labels_list[0]) == len(labels_list):
            return labels_list[0]
        # 當數據集中已經沒有可劃分的屬性,採用多數表決法返回類別
        if len(data_set[0]) == 1:
            return self._majority_cnt(labels_list)

        # 選擇最優劃分屬性
        best_feature = self._choose_best_feature_to_split(data_set)
        best_feature_label = feature_labels[best_feature]
        # 初始化樹
        tree = {best_feature_label: {}}
        # 移除已經選擇的劃分屬性
        del feature_labels[best_feature]

        # 獲取當前劃分屬性的所有取值
        feature_values = [sample[best_feature] for sample in data_set]
        # set() 可以去除 feature_values 中存在的相同值
        unique_values = set(feature_values)
        for value in unique_values:
            sub_feature_labels = feature_labels[:]
            tree[best_feature_label][value] = self.creat_tree(
                self._split_data_set(data_set, best_feature, value),
                sub_feature_labels)

        return tree

    def _calc_shannon_ent(self, data_set):
        """
        計算信息熵
        """
        # 計算數據集中樣本數量
        samples_num = len(data_set)
        labels_count = {}
        # 遍歷數據集中所有樣本,計算每種類別的樣本數量
        for sample in data_set:
            # 獲取樣本的標籤值,即類別
            cur_label = sample[-1]
            if cur_label not in labels_count.keys():
                labels_count[cur_label] = 0
            labels_count[cur_label] += 1
        shannon_ent = 0.0
        # 求信息熵
        for count in labels_count.values():
            prob = float(count)/samples_num
            shannon_ent -= prob*log(prob, 2)
        return shannon_ent

    def _split_data_set(self, data_set, axis, value):
        """
        按照給定特徵劃分數據集,返回數據集中滿足特徵值爲 value 的子集

        data_set:待劃分的數據集
        axis:劃分數據集的特徵
        value:劃分數據集的特徵值
        """
        sub_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:])
                sub_data_set.append(reduced_feat_vec)
        return sub_data_set

    def _choose_best_feature_to_split(self, data_set):
        """
        選擇最好的劃分屬性

        data_set:待劃分的數據集
        """
        # 獲取數據集的樣本數量
        samples_num = len(data_set)
        # 獲取樣本特徵值數量
        feature_num = len(data_set[0]) - 1
        # 計算數據集未劃分前的信息熵
        base_entropy = self._calc_shannon_ent(data_set)
        best_info_gain = 0.0
        best_feature = -1
        # 遍歷每一個特徵
        for i in range(feature_num):
            feature_values = [sample[i] for sample in data_set]
            unique_vals = set(feature_values)
            new_entropy = 0.0
            # 遍歷當前特徵的每一個值,並計算出劃分數據集後信息熵
            for value in unique_vals:
                sub_data_set = self._split_data_set(data_set, i, value)
                prob = float(len(sub_data_set))/samples_num
                new_entropy -= prob*self._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 _majority_cnt(self, labels_list):
        """
        返回數量最多的類型

        labels_list:類型列表
        """
        labels_count = {}
        for label in labels_list:
            if label not in labels_count.keys():
                labels_count[label] = 0
            labels_count[label] += 1
        sorted_labels_count = \
            sorted(labels_count.items(), key=lambda x: x[1], reverse=True)
        return sorted_labels_count[0][0]


def get_training_datas():
    data_set = [[1, 1, 'yes'],
                [1, 1, 'yes'],
                [1, 0, 'no'],
                [0, 1, 'no'],
                [0, 1, 'no']]
    feature_labels = ['no surfacing', 'flippers']
    return data_set, feature_labels


if __name__ == "__main__":
    data_set, feature_labels = get_training_datas()
    decision_tree_mode = DecisionTreeID3()
    decision_tree = decision_tree_mode.creat_tree(data_set, feature_labels)
    print(decision_tree)

疑問:

  1. C4.5 對離散型屬性是採用二分還是多分?

參考:
[1] 周志華《機器學習》
[2] 李航《統計學習方法》
[3] 《機器學習實戰》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章