ID3決策樹python程序實現

算法原理

決策樹是一類經典的機器學習方法,既可以用於分類任務,也可以用於迴歸。分類和迴歸對應的分別是分類樹和迴歸樹,本文將以最常見的一類決策樹——ID3分類樹爲例,講解模型的原理以及程序實現。

模型

已知一組有n個樣本的訓練集
{xi,yi}i=1n,xik=1tDk,yi{0,1} \{x_i,y_i\}_ {i=1}^n,x_i\in\prod_{k=1}^tD^k,y_i\in\{0,1\}
其中,每個樣本xix_i都有tt個特徵(爲了簡便起見,假設樣本的特徵都是離散值)。現在的目的是希望通過對訓練集進行訓練。之後往模型中代入新的樣本xjx_j,得到預測的分類類別。

原理

在講原理之前先通過一個例子引入,假設現在是晚上,你在考慮是否要玩遊戲,這時可能要先判斷現在是幾點,是否已經很晚了,如果很晚則不玩遊戲,若是現在還早,那可能還要判斷明天需要上交的作業寫好了沒有,還沒寫好則要抓緊時間寫,如果寫好了,就可以去玩遊戲了。這個例子的圖示可以表示爲

決策樹依次通過樣本的特徵自上而下地對樣本進行分類,根據某一個特徵的不同,可以將樣本分成若干組,對於樣本類別全都相同的組則停止分類,對於樣本類別不全相同的組,則需要根據剩餘的特徵再進行分類,直至最後得到的每個組裏的樣本類別全相同。

爲了理解決策樹的構建過程,需要先弄清楚幾個問題

樹的組成

決策樹包含節點和分支。

圖上的方框即代表節點,它是根據特徵進行分類得到的數據組。其中,節點包括根節點,中間節點和葉子節點。根節點是指最初始的節點,即還沒有進行任何分類的節點。葉子節點是指不用再進行分類的節點。不是根節點和葉子節點的節點便是中間節點。

分支是指由節點引出的若干條細支,每條分支都代表一個分類過程。

哪些特徵需要優先選擇

對應到決策樹中的節點,由於決策樹是自上而下構建的,並且每次只通過一個特徵進行分類。而樣本集中的特徵有很多,現在需要考慮的是要優先選擇哪一個特徵進行分類?按正常情況來講當然是分類後得到的各組數據的純度越高越好,這裏的純度體現在每組數據中各類別的分佈情況上。當每組數據中只有一種類別,則純度是最高的,當不止有一種類別,則純度會較低一些。衡量這種純度的方法有很多,本文介紹的是ID3算法,它是通過計算信息增益來選擇特徵的。

信息增益=分類前信息熵-分類後的加權信息熵
=Ei=1kDiDEi 信息增益=E-\sum_{i=1}^k\frac{|D_i|}{|D|}E_i
其中,EE是分類前的信息熵,D|D|指分類前的樣本總數,Di|D_i|是分類後的第i組數據的樣本總數,EiE_i是指分類後的第i組的信息熵。

爲了使得純度更高,我們希望分類後的信息熵更小,因此要選取信息增益最大的那個特徵。按照上述方法對類別數超過一的節點即數據集進行分類,直至得到的分組數據集中類別都只有一種。

分類樹與迴歸樹的區別

最明顯的不同就是分類樹輸出的是樣本的類別,屬於離散值,而回歸樹輸出的是實數,是連續值。另外,樣本的特徵也有離散值和連續值的差別,如果特徵是離散值,則在選取特徵時可以用普通的ID3、C4.5和Gini方法。如果特徵是連續值,則需要將該特徵的數值按大小進行排序,然後分別取這些數值作爲閾值將數據分成兩類,結合ID3、C4.5或Gini方法選出最優的特徵。

程序實現

各函數

計算信息熵

def cal_entropy(y):
    """信息熵計算
    
    參數
    -------
    y:類別編號 類型:narray, shape:{n_samples}
    
    返回
    ----
    e:信息熵  類型:float
    """
    count = np.array(pd.Series(y).value_counts())
    p = count/count.sum()
    return -np.sum(np.log2(p)*p)

選取最優的特徵

def choose_features_ID3(X, y):
    """選擇特徵(單個)
    
    參數
    ------
    X:特徵,類型:ndarray,shape:{n_samples, n_features}
    
    y: 類別編號 類型:narray, shape:{n_samples}
    
    返回
    -----
    min_fea_index:選出的特徵,類型:integer
    
    entropy:信息增益,類型:float
    """
    n_samples, n_features = X.shape
    
    fea_index = 0
    max_entropy = 0
    pre_y_entropy = cal_entropy(y)
    for i in range(n_features):
        entropy_sum = 0
        row_value = X[:,i]
        for value in set(row_value):
            bools = row_value==value
            entropy_sum += np.sum(bools)/n_samples * cal_entropy(y[bools])
        entropy = pre_y_entropy-entropy_sum
        if entropy>max_entropy:
            max_entropy = entropy
            fea_index = i
    return fea_index,entropy

構建ID3決策樹

def tree_ID3(X, y, X_name):
    """構建決策樹,採用ID3,無剪枝操作
    
    參數
    ------
    X:特徵,類型:ndarray,shape:{n_samples, n_features}
    
    y: 類別編號,類型:ndarray,shape:{n_samples}
    
    X_name: 特徵名,類型:ndarray,shape:{n_samples}
    """
    if not len(X):return 
    if cal_entropy(y)==0:return y[0]
    
    n_samples, n_features = X.shape
    index = choose_features_ID3(X, y)[0]
    dic = {X_name[index]:{}}
    remove_fea = X[:, index]
    for fea in set(remove_fea):
        row_bool = remove_fea==fea  # 留下的行索引
        col_bool = np.arange(n_features)!=index   # 留下的列索引
        dic[X_name[index]][fea] = tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
    return dic

實例化演示

以西瓜數據集爲例

dataSet = np.array([
        # 1
        ['青綠', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
        # 2
        ['烏黑', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', '好瓜'],
        # 3
        ['烏黑', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
        # 4
        ['青綠', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', '好瓜'],
        # 5
        ['淺白', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
        # 6
        ['青綠', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', '好瓜'],
        # 7
        ['烏黑', '稍蜷', '濁響', '稍糊', '稍凹', '軟粘', '好瓜'],
        # 8
        ['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '硬滑', '好瓜'],

        # ----------------------------------------------------
        # 9
        ['烏黑', '稍蜷', '沉悶', '稍糊', '稍凹', '硬滑', '壞瓜'],
        # 10
        ['青綠', '硬挺', '清脆', '清晰', '平坦', '軟粘', '壞瓜'],
        # 11
        ['淺白', '硬挺', '清脆', '模糊', '平坦', '硬滑', '壞瓜'],
        # 12
        ['淺白', '蜷縮', '濁響', '模糊', '平坦', '軟粘', '壞瓜'],
        # 13
        ['青綠', '稍蜷', '濁響', '稍糊', '凹陷', '硬滑', '壞瓜'],
        # 14
        ['淺白', '稍蜷', '沉悶', '稍糊', '凹陷', '硬滑', '壞瓜'],
        # 15
        ['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', '壞瓜'],
        # 16
        ['淺白', '蜷縮', '濁響', '模糊', '平坦', '硬滑', '壞瓜'],
        # 17
        ['青綠', '蜷縮', '沉悶', '稍糊', '稍凹', '硬滑', '壞瓜']
    ])
X = dataSet[:,:-1]
y = dataSet[:,-1]
X_name = np.array(['色澤','根蒂','敲聲','紋理','臍部','觸感'])
tree_ID3(X,y,X_name)

out

{'紋理': {'稍糊': {'觸感': {'硬滑': '壞瓜', '軟粘': '好瓜'}},
  '清晰': {'根蒂': {'稍蜷': {'色澤': {'青綠': '好瓜',
      '烏黑': {'觸感': {'硬滑': '好瓜', '軟粘': '壞瓜'}}}},
    '硬挺': '壞瓜',
    '蜷縮': '好瓜'}},
  '模糊': '壞瓜'}}

封裝成一個類

增加訓練函數fit和預測函數predict、check,其中,check函數是對一個樣本進行預測,predict函數整合了多個樣本。

class Tree_ID3:
    def __init__(self):
        pass
        
    def cal_entropy(self, y):
        """信息熵計算

        參數
        -------
        y:類別, 類型:narray, shape:{n_samples}

        返回
        ----
        e:信息熵, 類型:float
        """
        count = np.array(pd.Series(y).value_counts())
        # 每個類別的概率
        p = count/count.sum()
        # 信息熵
        return -np.sum(np.log2(p)*p)

    def choose_features_ID3(self, X, y):
        """選擇特徵(單個)

        參數
        ------
        X:特徵, 類型:ndarray, shape:{n_samples, n_features}

        y:類別, 類型:ndarray, shape:{n_samples}

        返回
        -----
        fea_index:選出的特徵, 類型:integer

        entropy:信息增益, 類型:float
        """
        n_samples, n_features = X.shape

        # 最優特徵的索引
        fea_index = 0
        # 最大的信息增益
        max_entropy = 0
        # 分類前標籤y的信息熵
        pre_y_entropy = self.cal_entropy(y)
        
        for i in range(n_features):
            # 初始化分類後的加權信息熵
            entropy_sum = 0
            row_value = X[:,i]
            for value in set(row_value):
                # 選中的樣本索引
                bools = row_value==value
                entropy_sum += np.sum(bools)/n_samples * self.cal_entropy(y[bools])
            # 當前信息增益
            entropy = pre_y_entropy-entropy_sum
            if entropy>max_entropy:
                max_entropy = entropy
                fea_index = i
        return fea_index,entropy

    def tree_ID3(self, X, y, X_name):
        """構建決策樹

        參數
        ------
        X:特徵, 類型:ndarray, shape:{n_samples, n_features}

        y:類別編號, 類型:ndarray, shape:{n_samples}

        X_name:特徵名, 類型:ndarray, shape:{n_samples}
        
        返回
        -----
        dic:決策樹, 類型:dict
        """
        if not len(X):return 
        # 只剩一類,返回
        if self.cal_entropy(y)==0:return y[0]

        n_samples, n_features = X.shape
        index = self.choose_features_ID3(X, y)[0]
        # 決策樹構建
        dic = {X_name[index]:{}}
        remove_fea = X[:, index]
        for fea in set(remove_fea):
            # 剩下的行索引
            row_bool = remove_fea==fea  
            # 剩下的列索引
            col_bool = np.arange(n_features)!=index   
            # 遞歸
            dic[X_name[index]][fea] = self.tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
        return dic 
    
    def check(self, tree, X, X_name):
        """預測
        """
        if not len(tree) or not len(X):return
        cur_fea_name = list(tree.keys())[0]
        cur_fea_index = np.where(X_name==cur_fea_name)[0][0]
        if X[cur_fea_index] not in tree[cur_fea_name].keys():return
        if tree[cur_fea_name][X[cur_fea_index]] in self.y_name:
            return tree[cur_fea_name][X[cur_fea_index]]
        else:
            bools = np.arange(len(X))!=cur_fea_index
            return self.check(tree[cur_fea_name][X[cur_fea_index]], X[bools], X_name[bools])
    
    def fit(self, X, y, X_name):
        self.X_name = X_name
        self.y_name = list(set(y))
        self.tree = self.tree_ID3(X, y, X_name)
        
    def predict(self, X):
        res = []
        for i in range(len(X)):
            res.append(self.check(self.tree, X[i], self.X_name))
        return np.array(res)

演示

clf = Tree_ID3()
clf.fit(X, y, X_name)
predict_y = clf.predict(X)
sum(predict_y==y)==len(y)

以上便是ID3分類樹的代碼實現,如有不對不妥之處,歡迎交流批評改正。

----end----

參考資料:
周志華《機器學習》

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