決策樹之ID3算法

1、決策樹之ID3算法簡述

  1976年-1986年,J.R.Quinlan給出ID3算法原型並進行了總結,肯定了決策樹學習的理論。這能夠看作是決策樹算法的起點。1993,Quinlan將ID3算法改進成C4.5算法,稱爲機器學習的十大算法之一。ID3算法的另外一個分支是CART(Classification adn Regression Tree, 分類迴歸決策樹),用於預測。這樣,決策樹理論徹底覆蓋了機器學習中的分類和迴歸兩個領域。python

  本文只作了ID3算法的回顧,所選數據的字段所有是有序多分類的分類變量。C4.5和CART有時間另花篇幅進行學習總結。本文須要有必定的pandas基礎、瞭解遞歸函數。算法

  一、ID3算法研究的核心思想是if-then,本質上是對數據進行分組操做。

  下表是包含用戶信息和購買決策的表。這張表已經對1024個樣本進行了分組統計。依此爲例解釋if-then(決策)和數據分組。json

  對於第0條和第7條數據,惟一的區別是income不一樣,因而能夠認爲,此時income不具備參考價值,而應考察student值或reputation的信息。因而:if-then定義了一套規則,用於肯定各個分類字段包含的信息計算方法,以及肯定優先按照哪一個字段進行分類決策。數據結構

  假如根據if-then,肯定優先按照age對數據集進行拆分,那麼能夠肯定三個水平(青年、中年、老年)對應的子數據集。而後,繼續對着三個子數據集分別再按照剩餘的字段進行拆分。如此循環直到除了購買決策以外的全部字段都被遍歷。你會發現,對於每一個拆分的子數據集,根本不須要關注裏面的值是漢字、字符串或數字,只須要關注有幾個類別便可。dom

  根據if-then的分類結果,對於一個樣本,能夠根據其各個字段的值和if-then規則來肯定它最終屬於下表哪一個組。機器學習

  決策樹強調分組字段具備順序性,認爲字段層級關係是層層遞進的;而咱們直接看這張表時,全部字段都是並排展開的,存在於同一層級。這或許是最大的區別。函數

  固然,你也能夠拿一個樣本,對照此表,找到它屬於的那個組,以及對應的purchase。若是purchase有不一樣值,根據count計算其機率便可。單元測試

  count age income student reputation purchase
0 64 青年 不買
1 64 青年 不買
2 128 中年
3 60 老年
4 64 老年
5 64 老年 不買
6 64 中年
7 128 青年 不買
8 64 青年
9 132 老年
10 64 青年
11 32 中年
12 32 中年
13 64 老年 不買

  二、肯定決策規則的兩個核心公式:經驗熵、條件熵和信息增益

  1948年,美國信息學家香農(Shannon)定義了信息熵:學習

    $$I(U) = log(\frac{1}{p}) = -log(p)$$測試

  I被稱爲不肯定性函數,表明事件的信息量。log表示取對數。假定對於一個信源,其發生各類事件是相互獨立的,而且其值具備可加性。所以使用log函數。可見,發生的機率越大,其不肯定性越低。

  考慮到信源的全部可能發生的事件,假設其機率爲$p_1, p_2,..., p_i$,則能夠計算其平均值(數學指望),該值被稱爲信息熵或者經驗熵。涵義即爲:一個信源的平均不肯定性,或者一個信源的不肯定性指望。用公式表示爲:

  $$H(D) = E[-log p_i] = -\sum_{i=1}^{n}p_{i}* $$

  舉個例子。計算purchase的信息熵(經驗熵):

init_dic = {
    "count": [64,64,128,60,64,64,64,128,64,132,64,32,32,64],
    "age": ["青年","青年","中年","老年","老年","老年","中年","青年","青年","老年","青年","中年","中年","老年"],
    "income": ["","","","","","","","","","","","","",""],
    "student": ["","","","","","","","","","","","","",""],
    "reputation": ["","","","","","","","","","","","","",""],
    "purchase": ["不買","不買","","","","不買","","不買","","","","","","不買"]
}
data = pd.DataFrame(init_dic, columns=["count", "age", "income", "student", "reputation", "purchase"])

# 計算買和不買的樣本數據
purchase_yes _count= data[data["purchase"] == ""]["count"].sum()
purchase_no_count = data[data["purchase"] == "不買"]["count"].sum()
# 計算各自的機率
purchase_yes_p = purchase_yes_count / (purchase_yes_count + purchase_no_count)
purchase_no_p = 1 - purchase_yes_p
print(purchase_yes_p, purchase_no_p)
# 計算此時的信息熵
I_purchase = -purchase_yes_p*np.log2(purchase_yes_p) -purchase_no_p*np.log2(purchase_no_p)
print(I_purchase)

# 0.625 0.375
# 0.954434002924965

  在X發生的狀況下,Y的熵稱爲條件熵H(Y|X)。顯然地,有公式:

  $$H(Y|X) = H(X,Y) - H(X) = \sum_{i} p(i) H(Y|X = x)$$

  上述公式表示:(X,Y)發生所包含的熵(它是個並集),減去X的熵,即爲Y發生「新」增的熵。條件熵的公式推導略。

  信息增益:表示得知特徵A的信息而使得D集合的信息不肯定性減小的程度。它爲集合D的經驗熵減去特徵A的條件熵。公式表示爲:

  $$g(D, A) = H(D) - H(D|A)$$

  聯合上面這兩個式子:

  $$g(D, A) = H(D) - H(D|A) = H(D) - (H(D,A) - H(A)) = H(D) + H(A) - H(D,A)$$

  它顯示是典型的計算兩個集合的交集公式,這能夠表示D和A之間的互信息。這行公式的理解相當重要。

  決策樹優先從信息增益大的特徵列開始劃分數據集。這樣要更「靠譜」,由於信息增益(互信息)最大,對集合D(實際上也就是決策標籤)影響力更大。

  計算age字段的(經驗)條件熵以及它的信息增益。

def shannon(data, column="age"):
    # 找到這個字段的惟一值
    levels = data[column].drop_duplicates().tolist()  # ['青年', '中年', '老年']
    # 計算該字段的全部數據集,顯然是整個數據集
    samples = data["count"].sum()
    # 依次計算信息熵
    entropy = 0
    for level in levels:
        # 獲取該水平的子數據集,計算買與不買的信息熵
        subdata = data[data[column] == level]
        purchase_yes = subdata[subdata["purchase"] == ""]["count"].sum()
        purchase_no = subdata[subdata["purchase"] == "不買"]["count"].sum()
        purchase_yes_p = purchase_yes / (purchase_yes + purchase_no)
        purchase_no_p = 1 - purchase_yes_p
        # 計算該水平上的信息熵
        if purchase_yes == 0 or purchase_no == 0: # 這裏要處理子數據集爲空的狀況;這裏暫未處理
            pass
        I_purchase = -purchase_yes_p*np.log2(purchase_yes_p) -purchase_no_p*np.log2(purchase_no_p)            
        # 計算該水平上的機率值
        level_p = subdata["count"].sum() / samples
        # 計算信息增益
        if I_purchase > 0:
            entropy += level_p * I_purchase
        # print(level, level_p, I_purchase, purchase_yes, purchase_no, entropy)
    return entropy

entropy_age = shannon(data, "age")
gain_age = I_purchase - entropy_age  # 計算這個字段的信息增益
print(gain_age)
# 0.2657121273840979
# 有報錯0除,沒作處理。本例只演示如何計算葉節點信息熵。

  三、決策樹流程

  決策樹的流程爲:

  (1)輸入須要分類的數據集和類別標籤和靶標籤。

  (2)檢驗數據集是否只有一列,或者是否最後一列(靶標籤數據默認放到最後一列)只有一個水平(惟一值)。

    是:返回惟一值水平或者佔比最大的那個水平

  (3)調用信息增益公式,計算全部節點的信息增益,獲得最大信息增益所對應的類別標籤。

  (4)創建決策樹字典用以保存當次葉節點數據信息。

  (5)進入循環:

    按照該類別標籤的不一樣水平,依次計算子數據集;

    對子數據集重複(1),(2),(3),(4),(5), (6)步。

  (6)返回決策樹字典。

  決策樹其實是一個大的遞歸函數,其結果是一個多層次的字典。

2、python3實現ID3算法

  一、python3實現ID3決策樹

  參照書上的代碼,用的數據結構不是列表而是pandas的DataFrame。數據文件下載地址:https://files.cnblogs.com/files/kuaizifeng/ID3data.txt.zip。

  信息熵和信息增益其實能夠提煉出來,做爲單獨的計算方法。方便替換其它的計算方式,如信息增益率,基尼不純度等。

  LoadDataSet用來載入數據,TreeHandler用來持久化數據。

  ID3Tree中,

    _best_split用來遍歷標籤並計算最大信息增益對應的標籤;

    _entropy就是計算熵;

    _split_dataSet用於切割數據集;

    _top_amount_level是遞歸終止條件觸發時的返回值。即只有一個特徵列的一個水平的子集時,若是對應的purchase還有買和不買(level),就返回最大佔比的level;

    mktree主程序,遞歸生成決策樹,並將其保存在tree字典中;

    predict主程序,用於預測分類;

    _unit_test,單元測試程序,用於測試上面一些函數。

import numpy as np
import pandas as pd
import json

class LoadDataSet(object):
    def load_dataSet(self):
        """數據文件下載地址:https://files.cnblogs.com/files/kuaizifeng/ID3data.txt.zip"""
        data = pd.read_csv("ID3data.txt", sep="\t", header=None)
        data.rename(columns={0: "age", 1: "income", 2: "student", 3: "reputation", 4: "purchase"}, inplace=True)
        return data
    
class TreeHandler(object):
    def __init__(self):
        self.tree = None
    def save(self, tree):
        self.tree = tree
        with open("tree.txt", mode="w", encoding="utf-8") as f:
            tree = json.dumps(tree, indent="  ", ensure_ascii=False)
            f.write(tree)
    def load(self, file):
        with open(file, mode="r", encoding="utf-8") as f:
            tree = f.read()
            self.tree = json.loads(tree)
        return self.tree    

class ID3Tree(LoadDataSet, TreeHandler):
    """主要的數據結構是pandas對象"""
    __count = 0
    def __init__(self):
        super().__init__()
        """認定最後一列是標籤列"""
        self.dataSet = self.load_dataSet()
        self.gain = {}
    
    def _entropy(self, dataSet):
        """計算給定數據集的熵"""
        labels= list(dataSet.columns)
        level_count = dataSet[labels[-1]].value_counts().to_dict()  # 統計分類標籤不一樣水平的值
        entropy = 0.0
        for key, value in level_count.items():
            prob = float(value) / dataSet.shape[0]
            entropy += -prob * np.log2(prob)
        return entropy
    
    def _split_dataSet(self, dataSet, column, level):
        """根據給定的column和其level來獲取子數據集"""
        subdata = dataSet[dataSet[column] == level]
        del subdata[column] # 刪除這個劃分字段列
        return subdata.reset_index(drop=True)  # 重建索引
    
    def _best_split(self, dataSet):
        """計算每一個分類標籤的信息增益"""
        best_info_gain = 0.0  # 求最大信息增益
        best_label = None     # 求最大信息增益對應的標籤(字段)
        labels = list(dataSet.columns)[: -1]     # 不包括最後一個靶標籤
        init_entropy = self._entropy(dataSet)  # 先求靶標籤的香農熵
        for _, label in enumerate(labels):
            # 根據該label(也即column字段)的惟一值(levels)來切割成不一樣子數據集,並求它們的香農熵
            levels = dataSet[label].unique().tolist()  # 獲取該分類標籤的不一樣level
            label_entropy = 0.0  # 用於累加各水平的信息熵;分類標籤的信息熵等於該分類標籤的各水平信息熵與其機率積的和。
            for level in levels: # 循環計算不一樣水平的信息熵
                level_data = dataSet[dataSet[label] == level]  # 獲取該水平的數據集
                prob = level_data.shape[0] / dataSet.shape[0]  # 計算該水平的數據集在總數據集的佔比
                # 計算香農熵,並更新到label_entropy中
                label_entropy += prob * self._entropy(level_data) # _entropy用於計算香農熵
            # 計算信息增益
            info_gain = init_entropy - label_entropy  # 代碼至此,已經可以循環計算每一個分類標籤的信息增益
            # 用best_info_gain來取info_gain的最大值,並獲取對應的分類標籤
            if info_gain > best_info_gain:  
                best_info_gain = info_gain
                best_label = label
            # 這裏保存一下每一次計算的信息增益,便於查看和檢查錯誤
            self.gain.setdefault(self.__count, {})  # 創建本次函數調用時的字段,設其value爲字典
            self.gain[self.__count][label] = info_gain  # 把本次函數調用時計算的各個標籤數據存到字典裏
        self.__count += 1
        return best_label
    
    def _top_amount_level(self, target_list):
        class_count = target_list.value_counts().to_dict()  # 計算靶標籤的不一樣水平的樣本量,並轉化爲字典
        # 字典的items方法能夠將鍵值對轉成[(), (), ...],可使用列表方法
        sorted_class_count = sorted(class_count.items(), key=lambda x:x[1], reverse=True)
        return sorted_class_count[0][0]
        
    def mktree(self, dataSet):
        """建立決策樹"""
        target_list = dataSet.iloc[:, -1]  # target_list 靶標籤的那一列數據
        # 程序終止條件一: 靶標籤(數據集的最後一列因變量)在該數據集上只有一個水平,返回該水平
        if target_list.unique().shape[0] <= 1:
            return target_list[0] # !!!
        # 程序終止條件二: 數據集只剩下把標籤這一列數據;返回數量最多的水平
        if dataSet.shape[1] == 1:
            return self._top_amount_level(target_list)
        # 不知足終止條件時,作以下遞歸處理
        # 1.選擇最佳分類標籤
        best_label = self._best_split(dataSet)   
        # 2.遞歸計算最佳分類標籤的不一樣水平的子數據集的信息增益
        #   各個子數據集的最佳分類標籤的不一樣水平...
        #   ... 
        #   直至遞歸結束
        best_label_levels = dataSet[best_label].unique().tolist()
        tree = {best_label: {}}    # 生成字典,用於保存樹狀分類信息;這裏不能用self.tree = {}存儲
        for level in best_label_levels:
            level_subdata = self._split_dataSet(dataSet, best_label, level)  # 獲取該水平的子數據集
            tree[best_label][level] = self.mktree(level_subdata)  # 返回結果
        return tree
    
    def predict(self, tree, labels, test_sample):
        """
        對單個樣本進行分類
        tree: 訓練的字典
        labels: 除去最後一列的其它字段
        test_sample: 須要分類的一行記錄數據
        """
        firstStr = list(tree.keys())[0]           # tree字典裏找到第一個用於分類鍵值對
        secondDict = tree[firstStr]
        featIndex = labels.index(firstStr)  # 找到第一個建(label)在給定label的索引
        for key in secondDict.keys():
            if test_sample[featIndex] == key:  # 找到test_sample在當前label下的值
                if secondDict[key].__class__.__name__ == "dict":
                    classLabel = self.predict(secondDict[key], labels, test_sample)
                else:
                    classLabel = secondDict[key]
        return classLabel
    
    def _unit_test(self):
        """用於測試_entropy函數"""
        data = [[1, 1, "yes"], 
                [1, 1, "yes"],
                [1, 0, "no"],
                [0, 1, "no"],
                [0, 1, "no"],]
        data = pd.DataFrame(data=data, columns=["a", "b", "c"])
        # return data # 到此行,用於測試_entropy
        # return self._split_dataSet(data, "a", 1)  # 到此行,用於測試_split_dataSet
        # return self._best_split(data)  # 到此行,用於測試_best_split
        # return self.mktree(self.dataSet)  # 到此行,用於測試主程序mktree
        self.tree = self.mktree(self.dataSet)  # 到此行,用於測試主程序mktree
        labels = ["age", "income", "student", "reputation"]
        test_sample = [0, 1, 0, 0]   # [0, 1, 0, 0, "no"]
        outcome = self.predict(self.tree, labels, test_sample)
        print("The truth class is %s, The ID3Tree outcome is %s." % ("no", outcome))

  測試代碼以下:

model = ID3Tree()
model._unit_test()
# print(json.dumps(model.gain, indent="  "))  # 能夠查看每次遞歸時的信息熵
# print(json.dumps(model.tree, indent="  "))  # 查看樹

# The truth class is no, The ID3Tree outcome is no.

  二、sklearn實現ID3算法

  sklearn將決策時算法分爲兩類:DecisionTreeClassifier和DecisionTreeRegressor。在實例化對象時,能夠選擇設置一些參數。DecisionTreeClassifier適用於分類變量,DecisionTreeRegressor適用於連續變量。

import sklearn
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(random_state=0, criterion="entropy", )
data = np.array(model.dataSet.iloc[:, :-1])  # model是上面代碼的model
target = np.array(model.dataSet.iloc[:, -1])
clf.fit(data, target)
clf.predict([data[0]])  # 預測第一條數據

# array(['no'], dtype=object)  # target[0]也爲no

  三、ID3的侷限性:

    1.ID3沒有考慮連續特徵

    2.ID3採用信息增益大的特徵優先創建決策樹的節點。在相同條件下,取值比較多的特徵比取值少的特徵信息增益大。

    3.ID3算法對於缺失值的狀況沒有作考慮

    4.沒有考慮過擬合的問題

相關文章
相關標籤/搜索