1. 程式人生 > >決策樹演算法及實現

決策樹演算法及實現

在電腦科學中,樹是一種很重要的資料結構,比如我們最為熟悉的二叉查詢樹(Binary Search Tree),紅黑樹(Red-Black Tree)等,通過引入樹這種資料結構,我們可以很快地縮小問題規模,實現高效的查詢。

在監督學習中,面對樣本中複雜多樣的特徵,選取什麼樣的策略可以實現較高的學習效率和較好的分類效果一直是科學家們探索的目標。那麼,樹這種結構到底可以如何用於機器學習中呢?我們先從一個遊戲開始。

我們應該都玩過或者聽過這麼一種遊戲:遊戲中,出題者寫下一個明星的名字,其他人需要猜出這個人是誰。當然,如果遊戲規則僅此而已的話,幾乎是無法猜出來的,因為問題的規模太大了。為了降低遊戲的難度,答題者可以向出題者問問題,而出題者必須準確回答是或者否,答題者依據回答提出下一個問題,如果能夠在指定次數內確定謎底,即為勝出。加入了問答規則之後,我們是否有可能猜出謎底呢?我們先實驗一下,現在我已經寫下了一個影視明星的名字,而你和我的問答記錄如下:

是男的嗎?Y
是亞洲人嗎?Y
是中國人嗎?N
是印度人嗎?Y
……
雖然只有短短四個問題,但是我們已經把答案的範圍大大縮小了,那麼接下,第5個問題你應該如何問呢?我相信你應該基本可以鎖定答案了,因為我看過的印度電影就那麼幾部。我們將上面的資訊結構化如下圖所示:

這裡寫圖片描述
在上面的遊戲中,我們針對性的提出問題,每一個問題都可以將我們的答案範圍縮小,在提問中和回答者有相同知識背景的前提下,得出答案的難度比我們想象的要小很多。

回到我們最初的問題中,如何將樹結構用於機器學習中?結合上面的圖,我們可以看出,在每一個節點,依據問題答案,可以將答案劃分為左右兩個分支,左分支代表的是Yes,右分支代表的是No,雖然為了簡化,我們只畫出了其中的一條路徑,但是也可以明顯看出這是一個樹形結構,這便是決策樹的原型。

  1. 決策樹演算法簡介

我們面對的樣本通常具有很多個特徵,正所謂對事物的判斷不能只從一個角度,那如何結合不同的特徵呢?決策樹演算法的思想是,先從一個特徵入手,就如同我們上面的遊戲中一樣,既然無法直接分類,那就先根據一個特徵進行分類,雖然分類結果達不到理想效果,但是通過這次分類,我們的問題規模變小了,同時分類後的子集相比原來的樣本集更加易於分類了。然後針對上一次分類後的樣本子集,重複這個過程。在理想的情況下,經過多層的決策分類,我們將得到完全純淨的子集,也就是每一個子集中的樣本都屬於同一個分類。
這裡寫圖片描述
比如上圖中,平面座標中的六個點,我們無法通過其x座標或者y座標直接就將兩類點分開。採用決策樹演算法思想:我們先依據y座標將六個點劃分為兩個子類(如水平線所示),水平線上面的兩個點是同一個分類,但是水平線之下的四個點是不純淨的。但是沒關係,我們對這四個點進行再次分類,這次我們以x左邊分類(見圖中的豎線),通過兩層分類,我們實現了對樣本點的完全分類。這樣,我們的決策樹的虛擬碼實現如下:

if y > a:
    output dot
else:
    if x < b:
          output cross
    else:
          output dot

由這個分類的過程形成一個樹形的判決模型,樹的每一個非葉子節點都是一個特徵分割點,葉子節點是最終的決策分類。如下圖所示:
這裡寫圖片描述
將新樣本輸入決策樹進行判決時,就是將樣本在決策樹上自頂向下,依據決策樹的節點規則進行遍歷,最終落入的葉子節點就是該樣本所屬的分類。

2 決策樹演算法流程

上面我們介紹決策樹演算法的思想,可以簡單歸納為如下兩點:

每次選擇其中一個特徵對樣本集進行分類
對分類後的子集遞迴進行步驟1
看起來是不是也太簡單了呢?實際上每一個步驟我們還有很多考慮的。在第一個步驟中,我們需要考慮的一個最重要的策略是,選取什麼樣的特徵可以實現最好的分類效果,而所謂的分類效果好壞,必然也需要一個評價的指標。在上文中,我們都用純淨來說明分類效果好,那何為純淨呢?直觀來說就是集合中樣本所屬類別比較集中,最理想的是樣本都屬於同一個分類。樣本集的純度可以用熵來進行衡量。

在資訊理論中,熵代表了一個系統的混亂程度,熵越大,說明我們的資料集純度越低,當我們的資料集都是同一個類別的時候,熵為0,熵的計算公式如下:
這裡寫圖片描述

其中,P(xi)表示概率,b在此處取2。比如拋硬幣的時候,正面的概率就是1/2,反面的概率也是1/2,那麼這個過程的熵為:
這裡寫圖片描述
可見,由於拋硬幣是一個完全隨機事件,其結果正面和反面是等概率的,所以具有很高的熵。假如我們觀察的是硬幣最終飛行的方向,那麼硬幣最後往下落的概率是1,往天上飛的概率是0,帶入上面的公式中,可以得到這個過程的熵為0,所以,熵越小,結果的可預測性就越強。在決策樹的生成過程中,我們的目標就是要劃分後的子集中其熵最小,這樣後續的的迭代中,就更容易對其進行分類。

既然是遞迴過程,那麼就需要制定遞迴的停止規則。在兩種情況下我們停止進一步對子集進行劃分,其一是劃分已經達到可以理想效果了,另外一種就是進一步劃分收效甚微,不值得再繼續了。用專業術語總結終止條件有以下幾個:

子集的熵達到閾值
子集規模夠小
進一步劃分的增益小於閾值
其中,條件3中的增益代表的是一次劃分對資料純度的提升效果,也就是劃分以後,熵減少越多,說明增益越大,那麼這次劃分也就越有價值,增益的計算公式如下:
這裡寫圖片描述
Gain
上述公式可以理解為:計算這次劃分之後兩個子集的熵之和相對劃分之前的熵減少了多少,需要注意的是,計運算元集的熵之和需要乘上各個子集的權重,權重的計算方法是子集的規模佔分割前父集的比重,比如劃分前熵為e,劃分為子集A和B,大小分別為m和n,熵分別為e1和e2,那麼增益就是e - m/(m + n) * e1 - n/(m + n) * e2。

  1. 決策樹演算法實現

有了上述概念,我們就可以開始開始決策樹的訓練了,訓練過程分為:

  1. 選取特徵,分割樣本集
  2. 計算增益,如果增益夠大,將分割後的樣本集作為決策樹的子節點,否則停止分割
  3. 遞迴執行上兩步
    上述步驟是依照ID3的演算法思想(依據資訊增益進行特徵選取和分裂),除此之外還有C4.5以及CART等決策樹演算法。
    演算法框架如下:
    class DecisionTree(object):
    def fit(self, X, y):
    # 依據輸入樣本生成決策樹
    self.root = self._build_tree(X, y)

    def _build_tree(self, X, y, current_depth=0):
    #1. 選取最佳分割特徵,生成左右節點
    #2. 針對左右節點遞迴生成子樹

    def predict_value(self, x, tree=None):
    # 將輸入樣本傳入決策樹中,自頂向下進行判定
    # 到達葉子節點即為預測值
    在上述程式碼中,實現決策樹的關鍵是遞迴構造子樹的過程,為了實現這個過程,我們需要做好三件事:分別是節點的定義,最佳分割特徵的選擇,遞迴生成子樹。

3.1 節點定義

決策樹的目的是用於分類預測,即各個節點需要選取輸入樣本的特徵,進行規則判定,最終決定樣本歸屬到哪一棵子樹,基於這個目的,決策樹的每一個節點需要包含以下幾個關鍵資訊:

判決特徵:當前節點針對哪一個特徵進行判決
判決規則:對於二類問題,這個規則一般是一個布林表示式
左子樹:判決為TRUE的樣本
右子樹:判決為FALSE的樣本
決策樹節點的定義程式碼如下所示:

class DecisionNode():
def init(self, feature_i=None, threshold=None,
value=None, true_branch=None, false_branch=None):
self.feature_i = feature_i # 用於測試的特徵對應的索引
self.threshold = threshold # 判斷規則:>=threshold為true
self.value = value # 如果是葉子節點,用於儲存預測結果
self.true_branch = true_branch # 左子樹
self.false_branch = false_branch # 右子樹

3.2 特徵選取

特徵選取是構造決策樹最關鍵的步驟,其目的是選出能夠實現分割結果最純淨的那個特徵,其操作流程的程式碼如下:

遍歷樣本集中的所有特徵,針對每一個特徵計算最佳分割點

再選取最佳的分割特徵

for feature_i in range(n_features):
# 遍歷集合中某個特徵的所有取值
for threshold in unique_values:
# 以當前特徵值作為閾值進行分割
Xy1, Xy2 = divide_on_feature(X_y, feature_i, threshold)
# 計算分割後的增益
gain = gain(y, y1, y2)
# 記錄最佳分割特徵,最佳分割閾值
if gain > largest_gain:
largest_gain = gain
best_criteria = {
“feature_i”: feature_i,
“threshold”: threshold
}

遍歷樣本集中的所有特徵,針對每一個特徵計算最佳分割點

再選取最佳的分割特徵

for feature_i in range(n_features):
    # 遍歷集合中某個特徵的所有取值
    for threshold in unique_values:
        # 以當前特徵值作為閾值進行分割
        Xy1, Xy2 = divide_on_feature(X_y, feature_i, threshold)
        # 計算分割後的增益
        gain = gain(y, y1, y2)
        # 記錄最佳分割特徵,最佳分割閾值
        if gain > largest_gain:
        largest_gain = gain
            best_criteria = {
                "feature_i": feature_i, 
                "threshold": threshold 
                }

3.3 節點分裂

節點分裂的時候有兩條處理分支,如果增益夠大,就分裂為左右子樹,如果增益很小,就停止分裂,將這個節點直接作為葉子節點。節點分裂和Gain(分割後增益)的計算可以做一個優化,在上一個步驟中,我們尋找最優分割點的時候其實就可以將最佳分裂子集和Gain計算並儲存下來,將上一步中的for迴圈改寫為:

以當前特徵值作為閾值進行分割

Xy1, Xy2 = divide_on_feature(X_y, feature_i, threshold)

計算分割後的熵

gain = gain(y, y1, y2)

記錄最佳分割特徵,最佳分割閾值

if gain > largest_gain:
largest_gain = gain
best_criteria = {
“feature_i”: feature_i,
“threshold”: threshold ,
}
best_sets = {
“left”: Xy1,
“right”: Xy2,
“gain”: gain
}

以當前特徵值作為閾值進行分割

Xy1, Xy2 = divide_on_feature(X_y, feature_i, threshold)

計算分割後的熵

gain = gain(y, y1, y2)

記錄最佳分割特徵,最佳分割閾值

if gain > largest_gain:
    largest_gain = gain
        best_criteria = {
        "feature_i": feature_i, 
        "threshold": threshold ,
        }
    best_sets = {
        "left": Xy1,
        "right": Xy2,
        "gain": gain
        }

為了防止過擬合,需要設定合適的停止條件,比如設定Gain的閾值,如果Gain比較小,就沒有必要繼續進行分割,所以接下來,我們就可以依據gain來決定分割策略:

if best_sets["gain"] > MIN_GAIN:
    # 對best_sets["left"]進一步構造子樹,並作為父節點的左子樹
    # 對best_sets["right"]進一步構造子樹,並作為父節點的右子樹

else:
    # 直接將父節點作為葉子節點

下面,我們結合一組實驗資料來學習決策樹的訓練方法。實驗資料來源於這裡,下表中的資料是一組消費調查結果,我們訓練決策樹的目的,就是構造一個分類演算法,使得有新的使用者資料時,我們依據訓練結果去推斷一個使用者是否購買這個商品:

AGE EDUCATION INCOME MARITAL STATUS PURCHASE?
36-55 master’s high single yes
18-35 high school low single no
36-55 master’s low single yes
18-35 bachelor’s high single no
< 18 high school low single yes
18-35 bachelor’s high married no
36-55 bachelor’s low married no

55 bachelor’s high single yes
36-55 master’s low married no
55 master’s low married yes
36-55 master’s high single yes
55 master’s high single yes
18 high school high single no
36-55 master’s low single yes
36-55 high school low single yes
< 18 high school low married yes
18-35 bachelor’s high married no
55 high school high married yes
55 bachelor’s low single yes
36-55 high school high married no
從表中可以看出,我們一共有20組調查樣本,每一組樣本包含四個特徵,分別是年齡段,學歷,收入,婚姻狀況,而最後一列是所屬分類,在這個問題中就代表是否購買了該產品。

監督學習就是在每一個樣本都有正確答案的前提下,用演算法預測結果,然後根據預測情況的好壞,調整演算法引數。在決策樹中,預測的過程就是依據各個特徵劃分樣本集,評價預測結果的好壞標準是劃分結果的純度。

為了方便處理,我們對樣本資料進行了簡化,將年齡特徵按照樣本的特點,轉化為離散的資料,比如小於18對應0,18對應1,18-35對應2,36-55對應3,大於55對應4,以此類推,同樣其他的特徵也一樣最數字化處理,教育水平分別對映為0(hight school),1(bachelor’s),2(master’s),收入對映為0(low)和1(hight), 婚姻狀況同樣對映為0(single), 1(married),最終處理後的樣本,放到一個numpy矩陣中,如下所示:

X_y = np.array(
        [[3, 2, 1, 0, 1],
         [2, 0, 0, 0, 0],
         [3, 2, 0, 0, 1],
         [2, 1, 1, 0, 0],
         [0, 0, 0, 0, 1],
         [2, 1, 1, 1, 0],
         [3, 1, 0, 1, 0],
         [4, 1, 1, 0, 1],
         [3, 2, 0, 1, 0],
         [4, 2, 0, 1, 1],
         [3, 2, 1, 0, 1],
         [4, 2, 1, 0, 1],
         [1, 0, 1, 0, 0],
         [3, 2, 0, 0, 1],
         [3, 0, 0, 0, 1],
         [0, 0, 0, 1, 1],
         [2, 1, 1, 1, 0],
         [4, 0, 1, 1, 1],
         [4, 1, 0, 0, 1],
         [3, 0, 1, 1, 0]]
    )
  1. 新樣本預測

依照上面的演算法構造決策樹,我們將決策樹打印出來,如下所示:

-- Classification Tree --
0 : 4? 
   T -> 1
   F -> 3 : 1? 
      T -> 0 : 2? 
            T -> 0
            F -> 1
      F -> 0 : 3? 
            T -> 1
            F -> 0 : 1? 
                        T -> 0
                        F -> 1

其中,冒號前代表選擇的分割特徵,冒號後面代表判別規則,二者組合起來就是一個決策樹的非葉子節點,每個非葉子節點進一步分割為分為True和False分支,對於葉子節點箭頭後面表示最終分類,0表示不購買,1表示購買。由於我們的資料做過簡化,所以上述結果不太直觀,我們將對應的特徵以及判斷規則翻譯一下:

年齡 : 大於55?
是 -> 購買
否 -> 收入 : 高?
是 -> 年齡 : 大於18?
是 -> 不購買
否 -> 購買
否 -> 年齡 : 大於36?
是 -> 購買
否 -> 年齡 : 大於等於18?
是 -> 不購買
否 -> 購買
決策樹構造完之後,我們就可以用來進行新樣本的分類了。決策樹的預測過程十分容易理解,只需要將從根節點開始,按照節點定義的規則進行判決,選擇對應的子樹,並重復這個過程,直到葉子節點即可。決策樹的預測功能實現程式碼如下:

def predict_value(self, x, tree=None):
        # 如果當前節點是葉子節點,直接輸出其值
        if tree.value is not None:
            return tree.value
        # 否則將x按照當前節點的規則進行判決
        # 如果判決為true選擇左子樹,否則選擇右子樹,
        feature_value = x[tree.feature_i]
        if feature_value >= tree.threshold:
            branch = tree.true_branch
        else:
            branch = tree.false_branch
        # 在選中的子樹上遞迴進行判斷
        return self.predict_value(x, branch)
  1. 總結

決策樹是一種簡單常用的分類器,通過訓練好的決策樹可以實現對未知的資料進行高效分類。從我們的例子中也可以看出,決策樹模型具有較好的可讀性和描述性,有助於輔助人工分析;此外,決策樹的分類效率高,一次構建後可以反覆使用,而且每一次預測的計算次數不超過決策樹的深度。

當然,決策樹也有其缺點。對於連續的特徵,比較難以處理,對於多分類問題,計算量和準確率都不理想。此外,在實際應用中,由於其最底層葉子節點是通過父節點中的單一規則生成的,所以通過手動修改樣本特徵比較容易欺騙分類器,比如在攔擊郵件識別系統中,使用者可能通過修改某一個關鍵特徵,就可以騙過垃圾郵件識別系統。從實現上來講,由於樹的生成採用的是遞迴,隨著樣本規模的增大,計算量以及記憶體消耗會變得越來越大。

此外,過擬合也是決策樹面臨的一個問題,完全訓練的決策樹(未進行剪紙,未限制Gain的閾值)能夠100%準確地預測訓練樣本,因為其是對訓練樣本的完全擬合,但是,對與訓練樣本以外的樣本,其預測效果可能會不理想,這就是過擬合。解決決策樹的過擬合,除了上文說到的通過設定Gain的閾值作為停止條件之外,通常還需要對決策樹進行剪枝,常用的剪枝策略有:

Pessimistic Error Pruning:悲觀錯誤剪枝
Minimum Error Pruning:最小誤差剪枝
Cost-Complexity Pruning:代價複雜剪枝
Error-Based Pruning:基於錯誤的剪枝,即對每一個節點,都用一組測試資料集進行測試,如果分裂之後,能夠降低錯誤率,再繼續分裂為兩棵子樹,否則直接作為葉子節點。
Critical Value Pruning:關鍵值剪枝,這就是上文中提到的設定Gain的閾值作為停止條件。
我們以最簡單的方式展示了ID3決策樹的實現方式,如果想要了解不同型別的決策樹的差別,可以參考這個連結。

另外,關於各種機器學習演算法的實現,強烈推薦參考Github倉庫ML-From-Scratch,下載程式碼之後,通過pip install -r requirements.txt安裝依賴庫即可執行程式碼。