本內容將介紹用於分類的決策樹(decision tree),以及 ID3、C4.5 和 CART 算法。
決策樹(decision tree)是一種基本的分類與迴歸方法。決策樹模型呈樹形結構,分爲分類樹、迴歸樹和模型樹,前者用於分類,後者用於預測實數值。其主要優點是模型具有可讀性,分類速度快。決策樹學習通常包括 3 個步驟:特徵選擇、決策樹的生成和決策樹的剪枝。這些決策樹學習的思想主要來源於由 Quinlan 在 1986 年提出的 ID3 算法和 1993 年提出的 C4.5 算法,以及由 Breiman 等人在 1984 年提出的 CART 算法。
本內容主要介紹分類決策樹,不涉及迴歸樹和模型樹。可以參閱 機器學習系列:決策樹 02 - CART 算法 瞭解 CART 算法和迴歸樹、模型樹。
一、決策樹模型和學習
1.1 決策樹模型
一棵決策樹包含一個根節點、若干個內部節點和若干個葉節點。圖-1 爲一棵決策樹,橢圓形爲判斷模塊,進行特徵判斷;矩形爲終止模塊,進行結果決策。從根節點到每個葉節點的路徑對應了一個判斷測試序列。
用決策樹進行預測,從根節點開始,對實例的某一特徵進行判斷,根據判斷結果,將實例分配到其子節點。如此遞歸地對實例進行判斷並分配,直至到達葉節點。最後根據葉節點對實例進行決策,分類樹輸出類別,迴歸樹輸出預測值。
圖-1 決策樹模型
1.2 決策樹學習
決策樹的學習,也稱爲決策樹的生成或決策樹的構造。決策樹學習過程:得到原始數據集,選擇最優劃分特徵,並根據此特徵劃分數據集,數據將被向下傳遞到子節點。如果子節點上的數據滿足特定條件,將構建葉節點;否則,再次選擇最優特徵劃分數據集。如此遞歸地劃分數據集,最終將形成一棵決策樹。決策樹的生成就是遞歸地構建樹的過程。
以分類樹進行說明,上面提到的特定條件爲:
- 當前節點包含的樣本全屬於同一類別,無需劃分。
- 當前可劃分屬性集爲空,或是所有樣本在所有屬性上取值相同,無法劃分。
- 當前節點包含的樣本集合爲空,不能劃分。
對上面提到的情況,構造葉節點的類別設定方法:
- 設定爲該節點所含樣本的類別(所有樣本爲同一類別)。
- 設定爲該節點所含樣本最多的類別(即採用多數表決法決定葉節點的類別)。
- 設定爲其父節點所含樣本最多的類別。
決策樹學習的目的是爲了產生一棵泛化能力強,即處理未見實例能力強的決策樹。其基本流程如下:
函數 create_tree() 的僞代碼大致如下:
create_tree():
if 數據集滿足特定條件:
將該節點存爲葉節點
return
end if
根據最優特徵劃分數據集,產生多個子集
for 每個劃分的子集:
if 子集爲空:
構造葉結點,其類別爲父節點所含樣本最多的類別
else:
調用函數 create_tree() 遞歸劃分子集
end if
end for
決策樹是一種貪心算法,它要在給定時間內做出最佳選擇,但並不關心能否達到全局最優。
接下來我們需要了解如何選擇最優劃分特徵,以及如何劃分數據集。
二、特徵選擇
特徵選擇在於選取對訓練數據具有分類能力的特徵。這樣可以提高決策樹學習的效率。通常特徵選擇的準則有信息增益、信息增益比和基尼指數等。在具體介紹它們之前,我們先來了解一下熵和條件熵。
2.1 熵與條件熵
2.1.1 熵
在信息論與概率統計中,熵(entropy,又被稱爲信息熵)是表示隨機變量不確定性的度量。假設 是一個取有限個值的離散隨機變量,其概率分佈爲
則隨機變量 的熵定義爲
約定若 時,。 的最小值爲 0,最大值爲 。通常,式(1)中的對數以 2 、自然常數或 10 爲底,熵的單位分別稱爲比特(bit)、納特(nat)或 Hart。熵越大,隨機變量的不確定性就越大。
2.1.2 條件熵
假設隨機變量 的聯合概率分佈爲
條件熵 表示在已知隨機變量 的條件下隨機變量 的不確定性。隨機變量 給定條件下 的條件熵(conditional entropy),定義爲 給定條件下 的條件概率分佈的熵對 的數學期望
當熵和條件熵中的概率由數據估計(特別是極大似然估計)得到時,所對應的熵和條件熵分別稱爲經驗熵(empirical entropy)和經驗條件熵(empirical conditional entropy)。
2.2 信息增益
信息增益(information gain)表示得知特徵 的信息而使得類 的信息的不確定性減少的程度。
特徵 對訓練數據集 的信息增益 ,定義爲集合 的經驗熵 與特徵 給定條件下 的經驗條件熵 之差,即
假設訓練數據集爲 ,存在以下設定:
- 爲其樣本容量,即樣本個數。
- 訓練數據集 有 個類 ,, 爲屬於類 的樣本個數。
- 特徵 有 個不同的取值 ,根據特徵 的取值將 劃分爲 個子集 , 爲 的樣本個數。
- 子集 中屬於類 的樣本的集合爲 , 爲 的樣本個數。
則使用特徵 劃分訓練數據集 獲得的信息增益爲
其中, 爲
ID3 算法就是使用信息增益來選擇劃分特徵。
2.3 信息增益率
信息增益準則對可取值數目較多的特徵有所偏好,爲減少這種偏好可能帶來的不利影響,可使用信息增益率(information gain ratio)來選擇最優劃分特徵。
其中, 爲
稱爲特徵 的“固有值”(intrinsic value)。特徵 的可能取值數目越多(即 越大),則 的值通常會越大。
C4.5算法就是使用信息增益率來選擇劃分特徵。 需注意的是,增益率準則對可取值數目較少的特徵有所偏好。因此,C4.5 算法並不是直接選擇增益率最大的候選劃分特徵,而是使用了一個啓發式:先從候選劃分特徵中找出信息增益高於平均水平的特徵,再從中選擇增益率最高的。
2.4 基尼指數
基尼指數(Gini index)定義爲
從上面的公式可知,基尼指數有兩種不同理解形式:
- 從式(8)來看, 表示從數據集 中隨機抽取兩個樣本,其類別標記不一致的概率。
- 從式(9)來看, 表示從數據集 中隨機選中的一個樣本被分錯的概率。
因此, 越小,則數據集 的純度越高。
則使用特徵 劃分訓練數據集後的基尼指數爲
其中 爲
CART 算法進行分類時,就是使用基尼指數來選擇劃分特徵。
三、劃分數據集方法
3.1 離散屬性
針對離散屬性,可以採用多分法或者二分法劃分數據集。假設有數據集 ,其中屬性 有 個不同的取值,根據屬性 的取值將數據集劃分爲 個子集,就是多分法;選擇其中的某個取值 ,根據該值將數據集劃分爲 2 個子集 和 , 包含屬性 不大於 的樣本, 包含屬性 大於 的樣本,就是二分法。
注意:如果採用多分法後,該屬性不能作爲其後代節點的劃分屬性;如果採用二分法,該屬性可以繼續作爲其後代節點的劃分屬性。
3.2 連續屬性
由於連續性屬性的可取值不再有限,因此,不能直接根據連續型屬性的可取值來對節點進行劃分。此時,連續型屬性離散化技術可派上用場。最簡單的策略是採用二分法(bi-partition)對連續屬性進行處理,C4.5 和 CART 決策樹算法都使用該方法。
注意:因爲連續屬性只能採用二分法,所以該屬性還可作爲其後代節點的劃分屬性。
ID3 算法不能處理連續屬性,C4.5 和 CART 算法可以處理連續型屬性。
四、決策樹的剪枝
在決策樹學習中,爲了儘可能對數據集進行正確分類,從而構建出過於複雜的決策樹。這樣產生的樹往往對訓練數據集的分類很正確,但對未知的測試數據的分類卻沒有那麼準確,即出現過擬合現象。可以通過降低對決策樹的複雜度(即去掉一些分支)來解決這個問題,這個過程稱之爲剪枝(pruning)。
決策樹剪枝有預剪枝(prepruning)和後剪枝(postpruning)兩種。預剪枝是指在決策樹生成過程中,對每個節點在劃分前後進行估計,若當前節點的劃分不能帶來決策樹泛化性能提升,則停止劃分並將當前節點標記爲葉節點。後剪枝則是先從訓練集生成一棵完整的決策樹;然後從上到下找到葉節點,用測試集來判斷將這些葉節點合併是否能降低測試誤差,如果能就進行合併。
決策樹的生成對應於模型的局部選擇,決策樹的剪枝對應於模型的全局選擇。決策樹的生成只考慮局部最優,相對地,決策樹的剪枝則考慮全局最優。
在 機器學習系列:決策樹 02 - CART 算法 裏面會講到剪枝的具體實現。
五、代碼實現
在生成分類決策樹時,ID3、C4.5 和 CART 三種算法的流程基本相同,基本上只需要在特徵選擇和特徵劃分時進行不同處理即可。下面給出 ID3 算法的 Python 代碼實現(Python 3.x):
from math import log
class DecisionTreeID3:
def __init__(self):
pass
def creat_tree(self, data_set, feature_labels):
"""
創建決策樹
data_set:創建決策樹的數據集
feature_labels:特徵名列表
"""
labels_list = [sample[-1] for sample in data_set]
# 數據集中的樣本屬於同一個類別,返回這個類別
if labels_list.count(labels_list[0]) == len(labels_list):
return labels_list[0]
# 當數據集中已經沒有可劃分的屬性,採用多數表決法返回類別
if len(data_set[0]) == 1:
return self._majority_cnt(labels_list)
# 選擇最優劃分屬性
best_feature = self._choose_best_feature_to_split(data_set)
best_feature_label = feature_labels[best_feature]
# 初始化樹
tree = {best_feature_label: {}}
# 移除已經選擇的劃分屬性
del feature_labels[best_feature]
# 獲取當前劃分屬性的所有取值
feature_values = [sample[best_feature] for sample in data_set]
# set() 可以去除 feature_values 中存在的相同值
unique_values = set(feature_values)
for value in unique_values:
sub_feature_labels = feature_labels[:]
tree[best_feature_label][value] = self.creat_tree(
self._split_data_set(data_set, best_feature, value),
sub_feature_labels)
return tree
def _calc_shannon_ent(self, data_set):
"""
計算信息熵
"""
# 計算數據集中樣本數量
samples_num = len(data_set)
labels_count = {}
# 遍歷數據集中所有樣本,計算每種類別的樣本數量
for sample in data_set:
# 獲取樣本的標籤值,即類別
cur_label = sample[-1]
if cur_label not in labels_count.keys():
labels_count[cur_label] = 0
labels_count[cur_label] += 1
shannon_ent = 0.0
# 求信息熵
for count in labels_count.values():
prob = float(count)/samples_num
shannon_ent -= prob*log(prob, 2)
return shannon_ent
def _split_data_set(self, data_set, axis, value):
"""
按照給定特徵劃分數據集,返回數據集中滿足特徵值爲 value 的子集
data_set:待劃分的數據集
axis:劃分數據集的特徵
value:劃分數據集的特徵值
"""
sub_data_set = []
for feat_vec in data_set:
if feat_vec[axis] == value:
reduced_feat_vec = feat_vec[:axis]
reduced_feat_vec.extend(feat_vec[axis+1:])
sub_data_set.append(reduced_feat_vec)
return sub_data_set
def _choose_best_feature_to_split(self, data_set):
"""
選擇最好的劃分屬性
data_set:待劃分的數據集
"""
# 獲取數據集的樣本數量
samples_num = len(data_set)
# 獲取樣本特徵值數量
feature_num = len(data_set[0]) - 1
# 計算數據集未劃分前的信息熵
base_entropy = self._calc_shannon_ent(data_set)
best_info_gain = 0.0
best_feature = -1
# 遍歷每一個特徵
for i in range(feature_num):
feature_values = [sample[i] for sample in data_set]
unique_vals = set(feature_values)
new_entropy = 0.0
# 遍歷當前特徵的每一個值,並計算出劃分數據集後信息熵
for value in unique_vals:
sub_data_set = self._split_data_set(data_set, i, value)
prob = float(len(sub_data_set))/samples_num
new_entropy -= prob*self._calc_shannon_ent(sub_data_set)
info_gain = base_entropy - new_entropy
# 獲取信息增益最高的劃分特徵和特徵值
if info_gain > best_info_gain:
best_info_gain = info_gain
best_feature = i
return best_feature
def _majority_cnt(self, labels_list):
"""
返回數量最多的類型
labels_list:類型列表
"""
labels_count = {}
for label in labels_list:
if label not in labels_count.keys():
labels_count[label] = 0
labels_count[label] += 1
sorted_labels_count = \
sorted(labels_count.items(), key=lambda x: x[1], reverse=True)
return sorted_labels_count[0][0]
def get_training_datas():
data_set = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
feature_labels = ['no surfacing', 'flippers']
return data_set, feature_labels
if __name__ == "__main__":
data_set, feature_labels = get_training_datas()
decision_tree_mode = DecisionTreeID3()
decision_tree = decision_tree_mode.creat_tree(data_set, feature_labels)
print(decision_tree)
疑問:
- C4.5 對離散型屬性是採用二分還是多分?
參考:
[1] 周志華《機器學習》
[2] 李航《統計學習方法》
[3] 《機器學習實戰》