一、決策樹模型
決策樹(Decision Tree)是一種常見的機器學習算法,而其核心便是“分而治之”的划分策略。例如,以西瓜為例,需要判斷一個西瓜是否是一個好瓜,那么就可以根據經驗考慮“西瓜是什么顏色?”,如果是“青綠色”,那么接着考慮“它的根蒂是什么形態?”,如果是“蜷縮”,那么再接着考慮“敲打聲音如何?”,如果是“濁響”,那么就可以判定這個西瓜可能是一個“好瓜”。

上圖便是一個決策樹模型基本決策流程。由上圖可見,決策樹模型通常包含一個根結點,若干中間節點和若干葉結點。其中,每個葉結點保存了當前特征分支\(x_i\)的標記\(y_i\),而其余的每個結點則保存了當前結點划分的特征和划分的依據(特征值)。
下面是決策樹模型訓練的偽代碼:
生成決策樹(D,F):# D為數據集,F為特征向量
生成一個結點node
1)如果無法繼續划分,標記為D中樣本數最多的類:
a. D中標記相同,無需划分
b. F為空,無法划分
c. D在F上取值相同,無需划分
2)如果可以繼續划分:
選擇最優划分屬性f;
遍歷D在f上的每一種取值f‘:
為node生成一個新的分支;
D‘為D在f上取值為f’的子集;
(1)如果D‘為空,將分支節點標記為D中樣本最多的類;
(2)如果D’不為空,生成決策樹(D',F \ {f})
二、選擇划分
由前面的偽代碼不難看出,生成決策樹的關鍵在於選擇最優的划分屬性。為了衡量各個特征划分的優劣,這里引入了純度(purity)的概念。即,希望隨着划分的進行,決策樹的分支節點所包含的樣本盡可能屬於同一類別。現在已經有了幾種用來度量樣本集純度的指標:
2.1 信息熵和信息增益
信息熵(information entropy)是度量樣本集純度的常用指標,其定義如下:
其中,\(p_k\)為\(D\)中第\(k\)類樣本所占比例,而\(Ent(D)\)越小,則說明樣本集的純度越高。
以一個選定的特征將樣本划分為\(v\)個子集,則可以分別計算出這\(v\)個子集的信息熵\(Ent(D^i)\),賦予每個子集權重\(\frac{|D^i|}{|D|}\)求出各個子集信息熵的加權平均值,便可以得到划分后的平均信息熵。定義信息增益(information gain):
那么,信息增益越大,說明樣本集以特征\(f\)進行划分獲得的純度提升越大(划分后子節點的平均信息熵越小)。著名的ID3決策樹便是采用信息增益作為標准來選擇划分屬性。
2.2 增益率
通過對信息熵公式進行分析不難發現,信息增益對於可取值數目較多的特征有所偏好。因為如果D在f上可取值較多,那么划分后各個子集的純度往往較高,信息熵也會較小,最后產生大量的分支,嚴重影響決策樹的泛化性能。所以C4.5決策樹在信息增益的基礎上引入了增益率(gain rate)的概念:
其中\(IV\)稱為屬性f的固有值(intrinsic value),屬性f的可取值數目越多,\(IV\)越大。不難看出,增益率對於屬性f可取值較少的屬性有所偏好,所以通常的策略是:先選擇信息增益高於平均水平的屬性,然后從中選擇增益率最小的屬性進行划分。
2.3 基尼指數
CART決策樹(分類回歸樹)采用基尼指數(Gini index)來選擇划分屬性。樣本集\(D\)的純度可用基尼值來度量,其定義如下:
不難看出,\(Gini(D)\)反映了從\(D\)中任意抽取兩個樣本,其類標不一致的概率。因此,基尼值越小,樣本集純度越高。那么屬性f的基尼指數則定義為:
因此,以基尼指數為評價標准的最優划分屬性為\(f^*=\arg\min_{f\in F}Gini\_index(D, f)\)。
三、剪枝
在訓練決策樹模型的時候,有時決策樹會將訓練集的一些特有性質當作一般性質進行了學習,從而產生過多的分支,不僅效率下降還可能導致過擬合(over fitting)從而降低泛化性能。剪枝(pruning)就是通過主動去掉決策樹的一些分支從而防止過擬合的一種手段。
3.1 預剪枝
預剪枝(prepruning)是指在生成決策樹的過程中,對每個結點划分前進行模擬,如果划分后不能帶來決策樹泛化性能的提升,則停止划分並將當前結點標記為葉結點。
3.2 后剪枝
后剪枝(post-pruning)則是指在生成一棵決策樹后,自下而上地對非葉結點進行考察,如果將該結點對應的子樹替換為葉結點能帶來泛化性能的提升,則進行替換。
3.3 剪枝示例
那么如何判斷模型的泛化性能是否提升?舉個栗子來說明(周志華《機器學習》P80-P83):假設生成的決策樹如下,

預留的驗證集如下:

以預剪枝為例,首先看根結點。假設不進行划分,將結點1標記為\(D\)中樣本最多的類別“是”,用驗證集進行評估,易得准確率為\(\frac{3}{7}=42.9\%\)。如果進行划分,則結點2、3、4將分別被標記為“是”、“是”、“否”,用驗證集評估則易得准確率為\(\frac{5}{7}=71.4\%>42.9\%\),所以繼續划分。再看結點2,划分前驗證集准確率為\(71.4\%\),划分后卻下降到了\(57.1\%\),所以不進行划分。以此為例,最終可以將前面的決策樹剪枝為如下形式:

而后剪枝則只是在生成決策樹后,從下往上開始判斷泛化性能,這里不再贅述(詳情可見周志華《機器學習》P82-P83)。后剪枝后決策樹形式如下:

3.4 預剪枝和后剪枝對比
項目 | 預剪枝 | 后剪枝 | 不剪枝 |
---|---|---|---|
時間 | 生成決策樹時 | 生成決策樹后 | \ |
方向 | 自上而下 | 自下而上 | \ |
效率 | 高 | 低 | 中 |
擬合度 | 欠擬合風險 | 擬合較好 | 過擬合風險 |
四、Python實現
4.1 基尼值和基尼指數
基尼值:
def _gini(self, y): # 基尼值
y_ps = []
y_unque = np.unique(y)
for y_u in y_unque:
y_ps.append(np.sum(y == y_u) / len(y))
return 1 - sum(np.array(y_ps) ** 2)
基尼指數:
def _gini_index(self, X, y, feature): # 特征feature的基尼指數
X_y = np.hstack([X, y.reshape(-1, 1)])
unique_feature = np.unique(X_y[:, feature])
gini_index = []
for uf in unique_feature:
sub_y = X_y[X_y[:, feature] == uf][:, X_y.shape[1] - 1]
gini_index.append(len(sub_y) / len(y) * self._gini(sub_y))
return sum(gini_index), feature
4.2 選擇划分特征
划分特征的選擇依賴於前面的基尼指數函數:
def _best_feature(self, X, y, features): # 選擇基尼指數最低的特征
return min([self._gini_index(X, y, feature) for feature in features], key=lambda x:x[0])[1]
4.3 后剪枝算法
def _post_pruning(self, X, y):
nodes_mid = [] # 棧,存儲所有中間結點
nodes = [self.root] # 隊列,用於輔助廣度優先遍歷
while nodes: # 通過廣度優先遍歷找到所有中間結點
node = nodes.pop(0)
if node.sub_node:
nodes_mid.append(node)
for sub in node.sub_node:
nodes.append(sub)
while nodes_mid: # 開始剪枝
node = nodes_mid.pop(len(nodes_mid) - 1)
y_pred = self.predict(X)
from sklearn.metrics import accuracy_score
score = accuracy_score(y, y_pred)
temp = node.sub_node
node.sub_node = None
if accuracy_score(y, self.predict(X)) <= score:
node.sub_node = temp
4.4 訓練算法
首先需要將數據集划分為訓練集和驗證集,訓練集用於訓練決策樹,驗證集用於后剪枝。訓練算法按照偽代碼編寫即可。
def fit(self, X, y):
# 將數據集划分為訓練集和驗證集
X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.7, test_size=0.3)
queue = [[self.root, list(range(X_train.shape[0])), list(range(X_train.shape[1]))]]
while queue: # 廣度優先生成樹
node, indexs, features = queue.pop(0)
node.y = ss.mode(y_train[indexs])[0][0] # 這里給每一個結點都添加了類標是為了防止測試集出現訓練集中沒有的特征值
# 如果樣本全部屬於同一類別
unique_y = np.unique(y_train[indexs])
if len(unique_y) == 1:
continue
# 如果無法繼續進行划分
if len(features) < 2:
if len(features) == 0 or len(np.unique(X_train[indexs, features[0]])) == 1:
continue
# 選擇最優划分特征
feature = self._best_feature(X_train[indexs], y_train[indexs], features)
node.feature = feature
features.remove(feature)
# 生成子節點
for uf in np.unique(X_train[indexs, feature]):
sub_node = Node(value=uf)
node.append(sub_node)
new_indexs = []
for index in indexs:
if X_train[index, feature] == uf:
new_indexs.append(index)
queue.append([sub_node, new_indexs, features])
self._post_pruning(X_valid, y_valid)
return self
4.6 導入鳶尾花數據集測試
導入鳶尾花數據集測試:
if __name__ == "__main__":
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import KBinsDiscretizer
from sklearn.metrics import classification_report
iris = datasets.load_iris()
X = iris.data
y = iris.target
X = KBinsDiscretizer(encode="ordinal").fit_transform(X) # 離散化
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, test_size=0.3)
classifier = DecisionTreeClassifier().fit(X_train, y_train)
y_pred = classifier.predict(X_test)
print(classification_report(y_test, y_pred))
分類報告:
