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


決策樹

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

決策樹學習的三個步驟:

  • 特征選擇

通常使用信息增益最大、信息增益比最大或基尼指數最小作為特征選擇的准則。

  • 樹的生成

決策樹的生成往往通過計算信息增益或其他指標,從根結點開始,遞歸地產生決策樹。這相當於用信息增益或其他准則不斷地選取局部最優的特征,或將訓練集分割為能夠基本正確分類的子集。

  • 樹的剪枝

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

常用的特征選擇准則:

(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:': {}}}}
樣本 [老年, 否, 是, 非常好] 的預測結果:是


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM