《統計學習方法》——從零實現決策樹

_蓑衣客發表於2021-03-17

決策樹

決策樹是一種樹形結構,其中每個內部節點表示一個屬性上的判斷,每個分支代表一個判斷結果的輸出,最後每個葉子節點代表一種分類結果。

決策樹學習的三個步驟:

  • 特徵選擇

通常使用資訊增益最大、資訊增益比最大或基尼指數最小作為特徵選擇的準則。

  • 樹的生成

決策樹的生成往往通過計算資訊增益或其他指標,從根結點開始,遞迴地產生決策樹。這相當於用資訊增益或其他準則不斷地選取區域性最優的特徵,或將訓練集分割為能夠基本正確分類的子集。

  • 樹的剪枝

由於生成的決策樹存在過擬合問題,需要對它進行剪枝,以簡化學到的決策樹。決策樹的剪枝,往往從已生成的樹上剪掉一些葉結點或葉結點以上的子樹,並將其父結點或根結點作為新的葉結點,從而簡化生成的決策樹。

常用的特徵選擇準則:

(1)資訊增益(ID3)

樣本集合\(D\)對特徵\(A\)的資訊增益定義為:

\[g(D, A)=H(D)-H(D|A) \]

\[H(D)=-\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|} \log _{2} \frac{\left|C_{k}\right|}{|D|} \]

\[H(D | A)=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} H\left(D_{i}\right) \]

(2)資訊增益比(C4.5)

樣本集合\(D\)對特徵\(A\)的資訊增益比定義為:

\[g_{R}(D, A)=\frac{g(D, A)}{H_A(D)} \]

\[H_A(D)=-\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|}log_2 \frac{\left|D_{i}\right|}{|D|} \]

其中,\(g(D,A)\)是資訊增益,\(H(D_A)\)是資料集\(D\)關於特徵值A的熵,n是特徵A取值的個數。

(3)基尼指數(CART)

樣本集合\(D\)的基尼指數(CART)

\[\operatorname{Gini}(D)=1-\sum_{k=1}^{K}\left(\frac{\left|C_{k}\right|}{|D|}\right)^{2} \]

特徵\(A\)條件下集合\(D\)的基尼指數:

\[\operatorname{Gini}(D, A)=\frac{\left|D_{1}\right|}{|D|} \operatorname{Gini}\left(D_{1}\right)+\frac{\left|D_{2}\right|}{|D|} \operatorname{Gini}\left(D_{2}\right) \]

ID3、C4.5和CART的區別

(1)適用範圍:

  • ID3和C4.5只能用於分類,CART還可以用於迴歸任務。

(2)樣本資料:

  • ID3只能處理離散的特徵,C4.5和CART可以處理連續變數的特徵(通過對資料排序之後找到類別不同的分割線作為切分點,根據切分點把連續屬性轉換為布林型, 從而將連續型變數轉換多個取值區間的離散型變數)
  • ID3對特徵的缺失值沒有考慮,C4.5和CART增加了對缺失值的處理(主要是兩個問題:樣本某些特徵缺失的情況下選擇劃分的屬性;選定了劃分屬性,對於在該屬性上缺失特徵的樣本的處理)
  • 從效率角度考慮,小樣本C4.5,大樣本CART。因為C4.5涉及到多次排序和對數運算,CART採用了簡化的二叉樹模型,在計算機中二叉樹模型會比多叉樹運算效率高,同時特徵選擇採用了近似的基尼係數來簡化計算。

(3)節點特徵選擇:

  • 在每個內部節點的特徵選擇上,ID3選擇資訊增益最大的特徵,C4.5選擇資訊增益比最大的特徵,CART選擇基尼指數最小的特徵及其切分點作為最優特徵和最優切分點。
  • ID3和C4.5節點上可以產出多叉,而CART節點上永遠是二叉
  • 特徵變數的使用中,對具有多個分類值的特徵ID3和C4.5在層級之間只單次使用,CART可多次重複使用

(4)剪枝

  • C4.5是通過剪枝(PEP)來減小模型複雜度增加泛化能力,而CART是對所有子樹中選取最優子樹(CCP)

用numpy實現ID3決策樹的程式碼如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from collections import defaultdict
from math import log


# 生成書上的資料
def create_data():
    datasets = [['青年', '否', '否', '一般', '否'],
               ['青年', '否', '否', '好', '否'],
               ['青年', '是', '否', '好', '是'],
               ['青年', '是', '是', '一般', '是'],
               ['青年', '否', '否', '一般', '否'],
               ['中年', '否', '否', '一般', '否'],
               ['中年', '否', '否', '好', '否'],
               ['中年', '是', '是', '好', '是'],
               ['中年', '否', '是', '非常好', '是'],
               ['中年', '否', '是', '非常好', '是'],
               ['老年', '否', '是', '非常好', '是'],
               ['老年', '否', '是', '好', '是'],
               ['老年', '是', '否', '好', '是'],
               ['老年', '是', '否', '非常好', '是'],
               ['老年', '否', '否', '一般', '否'],
               ]
    df = pd.DataFrame(datasets, columns=[u'年齡', u'有工作', u'有自己的房子', u'信貸情況', u'類別'])
    return df


# 計算資料集的熵,需D的最後一列為標籤
def entropy(D):
    total_num = len(D)
    label_cnt = defaultdict(int)
    for i in range(total_num):
        label = D[i][-1]
        label_cnt[label] += 1
    ent = -sum([(cnt/total_num) * log(cnt/total_num, 2) for cnt in label_cnt.values()])
    return ent


# 計算列索引為index的屬性對集合D的條件熵
def cond_entropy(D, index):
    total_num = len(D)
    feature_sets = defaultdict(list)
    for i in range(total_num):
        feature = D[i][index]
        feature_sets[feature].append(D[i])
    cond_ent = sum([(len(d)/total_num) * entropy(d) for d in feature_sets.values()])
    return cond_ent
# 決策樹節點類
class Node:
    def __init__(self, is_leaf, label=None, feature_idx=None, feature_name=None):
        self.is_leaf = is_leaf
        self.label = label  # 僅針對於葉子節點
        self.feature_idx = feature_idx  # 該節點特徵對應的列索引,僅針對於非葉子節點
        self.feature_name = feature_name  # 該節點特徵名,僅針對於非葉子節點
        self.sons = {}
        
    def add_son(self, feature_value, node):
        self.sons[feature_value] = node
        
    def predict(self, x):
        if self.is_leaf:
            return self.label
        return self.sons[x[self.feature_idx]].predict(x)
    
    def __repr__(self):
        s = {
            'feature:': self.feature_name,
            'label:': self.label,
            'sons:': self.sons
            }
        return '{}'.format(s)


# ID3決策樹
class ID3DTree:
    def __init__(self, epsilon=0.1):
        """
        epsilon: 決策樹停止生長的資訊增益閾值
        """
        self.epsilon = epsilon
        self.decision_tree = None
        
    def fit(self, data):
        """data為dataframe格式"""
        self.decision_tree = self._train(data)
        return self.decision_tree
        
    def predict(self, x):
        if self.decision_tree:
            return self.decision_tree.predict(x)
        
    def _get_max_gain_feature(self, D):
        num_features = len(D[0]) - 1
        ent_D = entropy(D)
        index, max_gain = 0, 0
        for i in range(num_features):
            cond_ent = cond_entropy(D, i)
            gain = ent_D - cond_ent  # 資訊增益 = H(D) - H(D|A)
            if gain > max_gain:
                max_gain = gain
                index = i
        return index, max_gain
            
    def _train(self, data):
        """遞迴構建決策樹,data為dataframe格式"""
        y, feature_names = data.iloc[:, -1], data.columns[:-1]
        # 1.資料集中所有樣本均屬於同一類別,則停止生長
        if len(y.value_counts()) == 1:
            return Node(is_leaf=True, label=y.iloc[0])
        
        # 2.資料集中特徵數量為空,則將包含例項數量最多的類作為該葉子節點的標籤
        if len(feature_names) == 0:
            label = y.value_counts().sort_values(ascending=False).index[0]
            return Node(is_leaf=True, label=label)
        
        # 計算資訊增益最大的特徵
        idx, max_gain = self._get_max_gain_feature(np.array(data))
        # 3. 如果最大的資訊增益小於設定的閾值,則停止生長
        if max_gain < self.epsilon:
            label = y.value_counts().sort_values(ascending=False).index[0]
            return Node(is_leaf=True, label=label)
        
        target_feature = feature_names[idx]  # 當前節點特徵
        curr_node = Node(is_leaf=False, feature_idx=idx, feature_name=target_feature)
        value_sets = data[target_feature].value_counts().index  # 該特徵取值集合
        for value in value_sets:
            sub_data = data.loc[data[target_feature]==value].drop([target_feature], axis=1)
            # 4.遞迴生成子樹
            sub_tree = self._train(sub_data)
            curr_node.add_son(value, sub_tree)
            
        return curr_node

測試程式碼:

df_data = create_data()
dt = ID3DTree()
tree = dt.fit(df_data)
print('決策樹:{}'.format(tree))

y_test = dt.predict(['老年', '否', '是', '非常好'])
print('樣本 [老年, 否, 是, 非常好] 的預測結果:{}'.format(y_test))

輸出為:

決策樹:{'feature:': '有自己的房子', 'label:': None, 'sons:': {'否': {'feature:': '有工作', 'label:': None, 'sons:': {'否': {'feature:': None, 'label:': '否', 'sons:': {}}, '是': {'feature:': None, 'label:': '是', 'sons:': {}}}}, '是': {'feature:': None, 'label:': '是', 'sons:': {}}}}
樣本 [老年, 否, 是, 非常好] 的預測結果:是

相關文章