決策樹之ID3算法
一、決策樹之ID3算法簡述
1976年-1986年,J.R.Quinlan給出ID3算法原型並進行了總結,確定了決策樹學習的理論。這可以看做是決策樹算法的起點。1993,Quinlan將ID3算法改進成C4.5算法,稱為機器學習的十大算法之一。ID3算法的另一個分支是CART(Classification adn Regression Tree, 分類回歸決策樹),用於預測。這樣,決策樹理論完全覆蓋了機器學習中的分類和回歸兩個領域。
本文只做了ID3算法的回顧,所選數據的字段全部是有序多分類的分類變量。C4.5和CART有時間另花篇幅進行學習總結。本文需要有一定的pandas基礎、了解遞歸函數。
1、ID3算法研究的核心思想是if-then,本質上是對數據進行分組操作。
下表是包含用戶信息和購買決策的表。這張表已經對1024個樣本進行了分組統計。依此為例解釋if-then(決策)和數據分組。
對於第0條和第7條數據,唯一的區別是income不同,於是可以認為,此時income不具有參考價值,而應考察student值或reputation的信息。於是:if-then定義了一套規則,用於確定各個分類字段包含的信息計算方法,以及確定優先按照哪個字段進行分類決策。
假如根據if-then,確定優先按照age對數據集進行拆分,那麽可以確定三個水平(青年、中年、老年)對應的子數據集。然後,繼續對著三個子數據集分別再按照剩余的字段進行拆分。如此循環直到除了購買決策之外的所有字段都被遍歷。你會發現,對於每個拆分的子數據集,根本不需要關註裏面的值是漢字、字符串或數字,只需要關註有幾個類別即可。
根據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 | 老年 | 中 | 否 | 優 | 不買 |
2、確定決策規則的兩個核心公式:經驗熵、條件熵和信息增益
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除,沒做處理。本例只演示如何計算葉節點信息熵。
3、決策樹流程
決策樹的流程為:
(1)輸入需要分類的數據集和類別標簽和靶標簽。
(2)檢驗數據集是否只有一列,或者是否最後一列(靶標簽數據默認放到最後一列)只有一個水平(唯一值)。
是:返回唯一值水平或者占比最大的那個水平
(3)調用信息增益公式,計算所有節點的信息增益,得到最大信息增益所對應的類別標簽。
(4)建立決策樹字典用以保存當次葉節點數據信息。
(5)進入循環:
按照該類別標簽的不同水平,依次計算子數據集;
對子數據集重復(1),(2),(3),(4),(5), (6)步。
(6)返回決策樹字典。
決策樹實際上是一個大的遞歸函數,其結果是一個多層次的字典。
二、python3實現ID3算法
1、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.
2、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
3、ID3的局限性:
1.ID3沒有考慮連續特征
2.ID3采用信息增益大的特征優先建立決策樹的節點。在相同條件下,取值比較多的特征比取值少的特征信息增益大。
3.ID3算法對於缺失值的情況沒有做考慮
4.沒有考慮過擬合的問題
決策樹之ID3算法