閱讀本文需要的背景知識點:一丟丟程式設計知識
一、引言
在生活中,每次到飯點時都會在心裡默唸——“等下吃啥?”,可能今天工作的一天了不想走遠了,這時我們會決定餐廳的距離不能超過兩百米,再看看自己錢包裡的二十塊錢,決定吃的東西不能超過二十,最後點了份蘭州拉麵。從上面的例子中可以看到,我們今天吃蘭州拉麵都是由前面一系列的決策所決定的。
<center>圖1-1</center>
如圖1-1所示,將上面的決策過程用一顆二叉樹來表示,這個樹就被稱為決策樹(Decision Tree)。在機器學習中,同樣可以通過資料集訓練出如圖1-1所示的決策樹模型,這種演算法被稱為決策樹學習演算法(Decision Tree Learning)1 。
二、模型介紹
模型
決策樹學習演算法(Decision Tree Learning),首先肯定是一個樹狀結構,由內部結點與葉子結點組成,內部結點表示一個維度(特徵),葉子結點表示一個分類。結點與結點之間通過一定的條件相連線,所以決策樹又可以看成一堆if...else...規則的集合。
<center>圖2-1</center>
如圖2-1所示,其展示了一顆基本的決策樹資料結構與其包含的決策方法。
特徵選擇
既然要做決策,需要決定的就是從哪個維度(特徵)來做決策,例如前面例子中的店鋪距離、錢包零錢數等。在機器學習中我們需要一個量化的指標來確定使用的特徵更加合適,即使用該特徵劃分後,得到的子集合的“純度”更高。這時引入三種指標——資訊增益(Information Gain)、基尼指數(Gini Index)、均方差(MSE)來解決前面說的問題。
資訊增益(Information Gain)
式2-1是一種表示樣本集純度的指標,被稱為資訊熵(Information Entropy),其中 D 表示樣本集, K 表示樣本集分類數,p_k 表示第 k 類樣本在樣本集所佔比例。Ent(D) 的值越小,樣本集的純度越高。
$$ \operatorname{Ent}(D)=-\sum_{k=1}^{K} p_{k} \log _{2} p_{k} $$
<center>式2-1</center>
式2-2表示用一個離散屬性劃分後對樣本集的影響,被稱為資訊增益(Information Gain),其中 D 表示樣本集,a 表示離散屬性,V 表示離散屬性 a 所有可能取值的數量,D^v 表示樣本集中第 v 種取值的子樣本集。
$$ \operatorname{Gain}(D, a)=\operatorname{Ent}(D)-\sum_{v=1}^{V} \frac{\left|D^{v}\right|}{|D|} \operatorname{Ent}\left(D^{v}\right) $$
<center>式2-2</center>
當屬性是連續屬性時,其可取值不像離散屬性那樣是有限的,這時可以將連續屬性在樣本集中的值排序後倆倆取平均值作為劃分點,改寫一下式2-2,得到如式2-3的結果,其中 T_a 表示平均值集合,D_t^v 表示子集合,當 v = - 時表示樣本中小於均值 t 的樣本子集,當 v = + 時表示樣本中大於均值t的樣本子集,取劃分點中最大的資訊增益作為該屬性的資訊增益值。
$$ \begin{aligned} T_{a} &=\left\{\frac{a^{i}+a^{i+1}}{2} \mid 1 \leq i \leq n-1\right\} \\ \operatorname{Gain}(D, a) &=\max _{t \in T_{a}} \operatorname{Gain}(D, a, t) \\ &=\max _{t \in T_{a}} \operatorname{Ent}(D)-\sum_{v \in\{-,+\}} \frac{\left|D_{t}^{v}\right|}{|D|} \operatorname{Ent}\left(D_{t}^{v}\right) \end{aligned} $$
<center>式2-3</center>
Gain(D, a) 的值越大,樣本集按該屬性劃分後純度的提升越高。由此可找到最合適的劃分屬性,如式2-4所示:
$$ a_{\text {best }}=\underset{a}{\operatorname{argmax}} \operatorname{Gain}(D, a) $$
<center>式2-4</center>
基尼指數(Gini Index)
式2-5是另一種表示樣本集純度的指標,被稱為基尼值(Gini),其中 D 表示樣本集, K 表示樣本集分類數,p_k 表示第 k 類樣本在樣本集所佔比例。Gini(D) 的值越小,樣本集的純度越高。
$$ \operatorname{Gini}(D)=1-\sum_{k=1}^{K} p_{k}^{2} $$
<center>式2-5</center>
式2-6表示用一個離散屬性劃分後對樣本集的影響,被稱為基尼指數(Gini Index),其中 D 表示樣本集,a 表示離散屬性,V 表示離散屬性 a 所有可能取值的數量,D^v 表示樣本集中第 v 種取值的子樣本集。
$$ \operatorname{Gini_{-}index}(D, a)=\sum_{v=1}^{V} \frac{\left|D^{v}\right|}{|D|} \operatorname{Gini}\left(D^{v}\right) $$
<center>式2-6</center>
同式2-3一樣,將連續屬性排序後倆倆取平均值作為劃分點,改寫式2-6,得到如式2-7的結果,其中 T_a 表示平均值集合,D_t^v 表示子集合,當 v = - 時表示樣本中小於均值 t 的樣本子集,當 v = + 時表示樣本中大於均值t的樣本子集,取劃分點中最小的基尼指數作為該屬性的基尼指數值。
$$ \operatorname{Gini_{-}index}(D, a)=\min _{t \in T_{a}} \sum_{v \in\{-,+\}} \frac{\left|D_{t}^{v}\right|}{|D|} \operatorname{Gini}\left(D_{t}^{v}\right) $$
<center>式2-7</center>
Gini_index(D, a) 的值越小,樣本集按該離散屬性劃分後純度的提升越高。由此可找到最合適的劃分屬性,如式2-8所示:
$$ a_{\text {best }}=\underset{a}{\operatorname{argmin}} \operatorname{Gini\_index}(D, a) $$
<center>式2-8</center>
均方差(MSE)
前面兩種指標使得決策樹可以用來做分類問題,那麼決策樹如果用來做迴歸問題時,就需要不同的指標來決定劃分的特徵,這個指標就是如式2-9所示的均方差(MSE),其中T_a 表示平均值集合,y_t^v 表示子集合標籤,當 v = - 時表示樣本中小於均值 t 的樣本子集標籤,當 v = + 時表示樣本中大於均值t的樣本子集標籤,後一項為對應子集合標籤的均值。
$$ \operatorname{MSE}(D, a)=\min _{t \in T_{a}} \sum_{v \in\{-,+\}}\left(y_{t}^{v}-\hat{y_{t}^{v}}\right)^{2} $$
<center>式2-9</center>
MSE(D, a) 的值越小,決策樹對樣本集的擬合程度越高。由此可找到最合適的劃分屬性,如式2-10所示:
$$ a_{\text {best }}=\underset{a}{\operatorname{argmin}} \operatorname{MSE}(D, a) $$
<center>式2-10</center>
知道了決策樹模型的資料結構,又知道如何劃分最佳的資料集,那麼接下來就來學習如何生成一顆決策樹。
三、演算法步驟
既然決策樹的資料結構是一顆樹,其子結點也必然是一顆樹,可以通過遞迴的方式來生成決策樹,步驟如下:
生成新結點 node;
當樣本中只存在一種分類C時:
將結點 node 標記為分類C的葉子結點,返回結點 node;
遍歷所有特徵:
計算當前特徵的資訊增益或基尼指數或均方差;
結點 node 中記錄最佳的劃分特徵;
按照最佳特徵劃分後左邊部分遞迴呼叫當前方法,當作結點 node 的左子結點;
按照最佳特徵劃分後右邊部分遞迴呼叫當前方法,當作結點 node 的右子結點;
返回結點 node;
四、正則化
當遞迴的生成決策樹時,模型對訓練資料的分類會非常準確,但是對未知的預測資料的表現並不理想,這就是所謂的過擬合的現象,這時可以同前面線性迴歸學習到的應對過擬合的解決方法一樣,對模型進行正則化。
決策樹的深度
可以通過限制決策樹的最大深度來達到對其正則化的效果,防止決策樹過擬合。這時只需在演算法步驟中加上一個用於記錄當前遞迴下樹深度的引數,當到達預設的最大深度時,不再生成新的子結點,將當前結點標記為樣本中分類佔比最大的分類並退出當前遞迴。
決策樹的葉子結點大小
另一個對決策樹進行正則化的方法是限制葉子結點最少包含的樣本數量,同樣可以防止過擬合的現象。當結節包含的樣本數,將當前結點標記為樣本中分類佔比最大的分類並退出當前遞迴
決策樹的剪枝
還可以通過對決策樹進行剪枝來防止其過擬合,將多餘的子樹剪斷。剪枝的方法又分成兩種,分別為預剪枝(prepruning)、後剪枝(post-pruning)。
預剪枝
顧名思義,預剪枝就是在生成決策樹的時候就決定是否生成子結點,判斷的方法為使用驗證資料集比較生成子結點與不生成子結點的精度,當生成子結點的精度有提升,則生成子結點,反之則不生成子結點。
<center>圖4-1 圖片來源於周志華《機器學習》</center>
後剪枝
後剪枝則是先生成一個完整的決策樹,然後再從葉子結點開始,同預剪枝一樣的判斷方法,當生成子結點的精度有提升,則保留子結點,反之則將子結點剪斷。
<center>圖4-2 圖片來源於周志華《機器學習》</center>
五、程式碼實現
使用 Python 實現基於資訊增益的決策樹分類:
import numpy as np
class GainNode:
"""
分類決策樹中的結點
基於資訊增益-Information Gain
"""
def __init__(self, feature=None, threshold=None, gain=None, left=None, right=None):
# 結點劃分的特徵下標
self.feature = feature
# 結點劃分的臨界值,當結點為葉子結點時為分類值
self.threshold = threshold
# 結點的資訊增益值
self.gain = gain
# 左結點
self.left = left
# 右結點
self.right = right
class GainTree:
"""
分類決策樹
基於資訊增益-Information Gain
"""
def __init__(self, max_depth = None, min_samples_leaf = None):
# 決策樹最大深度
self.max_depth = max_depth
# 決策樹葉結點最小樣本數
self.min_samples_leaf = min_samples_leaf
def fit(self, X, y):
"""
分類決策樹擬合
基於資訊增益-Information Gain
"""
y = np.array(y)
self.root = self.buildNode(X, y, 0)
return self
def buildNode(self, X, y, depth):
"""
構建分類決策樹結點
基於資訊增益-Information Gain
"""
node = GainNode()
# 當沒有樣本時直接返回
if len(y) == 0:
return node
y_classes = np.unique(y)
# 當樣本中只存在一種分類時直接返回該分類
if len(y_classes) == 1:
node.threshold = y_classes[0]
return node
# 當決策樹深度達到最大深度限制時返回樣本中分類佔比最大的分類
if self.max_depth is not None and depth >= self.max_depth:
node.threshold = max(y_classes, key=y.tolist().count)
return node
# 當決策樹葉結點樣本數達到最小樣本數限制時返回樣本中分類佔比最大的分類
if self.min_samples_leaf is not None and len(y) <= self.min_samples_leaf:
node.threshold = max(y_classes, key=y.tolist().count)
return node
max_gain = -np.inf
max_middle = None
max_feature = None
# 遍歷所有特徵,獲取資訊增益最大的特徵
for i in range(X.shape[1]):
# 計算特徵的資訊增益
gain, middle = self.calcGain(X[:,i], y, y_classes)
if max_gain < gain:
max_gain = gain
max_middle = middle
max_feature = i
# 資訊增益最大的特徵
node.feature = max_feature
# 臨界值
node.threshold = max_middle
# 資訊增益
node.gain = max_gain
X_lt = X[:,max_feature] < max_middle
X_gt = X[:,max_feature] > max_middle
# 遞迴處理左集合
node.left = self.buildNode(X[X_lt,:], y[X_lt], depth + 1)
# 遞迴處理右集合
node.right = self.buildNode(X[X_gt,:], y[X_gt], depth + 1)
return node
def calcMiddle(self, x):
"""
計算連續型特徵的倆倆平均值
"""
middle = []
if len(x) == 0:
return np.array(middle)
start = x[0]
for i in range(len(x) - 1):
if x[i] == x[i + 1]:
continue
middle.append((start + x[i + 1]) / 2)
start = x[i + 1]
return np.array(middle)
def calcEnt(self, y, y_classes):
"""
計算資訊熵
"""
ent = 0
for j in range(len(y_classes)):
p = len(y[y == y_classes[j]])/ len(y)
if p != 0:
ent = ent + p * np.log2(p)
return -ent
def calcGain(self, x, y, y_classes):
"""
計算資訊增益
"""
x_sort = np.sort(x)
middle = self.calcMiddle(x_sort)
max_middle = -np.inf
max_gain = -np.inf
ent = self.calcEnt(y, y_classes)
# 遍歷每個平均值
for i in range(len(middle)):
y_gt = y[x > middle[i]]
y_lt = y[x < middle[i]]
ent_gt = self.calcEnt(y_gt, y_classes)
ent_lt = self.calcEnt(y_lt, y_classes)
# 計算資訊增益
gain = ent - (ent_gt * len(y_gt) / len(x) + ent_lt * len(y_lt) / len(x))
if max_gain < gain:
max_gain = gain
max_middle = middle[i]
return max_gain, max_middle
def predict(self, X):
"""
分類決策樹預測
"""
y = np.zeros(X.shape[0])
self.checkNode(X, y, self.root)
return y
def checkNode(self, X, y, node, cond = None):
"""
通過分類決策樹結點判斷分類
"""
# 當沒有子結點時,直接返回當前臨界值
if node.left is None and node.right is None:
return node.threshold
X_lt = X[:,node.feature] < node.threshold
if cond is not None:
X_lt = X_lt & cond
# 遞迴判斷左結點
lt = self.checkNode(X, y, node.left, X_lt)
if lt is not None:
y[X_lt] = lt
X_gt = X[:,node.feature] > node.threshold
if cond is not None:
X_gt = X_gt & cond
# 遞迴判斷右結點
gt = self.checkNode(X, y, node.right, X_gt)
if gt is not None:
y[X_gt] = gt
使用 Python 實現基於基尼指數的決策樹分類:
import numpy as np
class GiniNode:
"""
分類決策樹中的結點
基於基尼指數-Gini Index
"""
def __init__(self, feature=None, threshold=None, gini_index=None, left=None, right=None):
# 結點劃分的特徵下標
self.feature = feature
# 結點劃分的臨界值,當結點為葉子結點時為分類值
self.threshold = threshold
# 結點的基尼指數值
self.gini_index = gini_index
# 左結點
self.left = left
# 右結點
self.right = right
class GiniTree:
"""
分類決策樹
基於基尼指數-Gini Index
"""
def __init__(self, max_depth = None, min_samples_leaf = None):
# 決策樹最大深度
self.max_depth = max_depth
# 決策樹葉結點最小樣本數
self.min_samples_leaf = min_samples_leaf
def fit(self, X, y):
"""
分類決策樹擬合
基於基尼指數-Gini Index
"""
y = np.array(y)
self.root = self.buildNode(X, y, 0)
return self
def buildNode(self, X, y, depth):
"""
構建分類決策樹結點
基於基尼指數-Gini Index
"""
node = GiniNode()
# 當沒有樣本時直接返回
if len(y) == 0:
return node
y_classes = np.unique(y)
# 當樣本中只存在一種分類時直接返回該分類
if len(y_classes) == 1:
node.threshold = y_classes[0]
return node
# 當決策樹深度達到最大深度限制時返回樣本中分類佔比最大的分類
if self.max_depth is not None and depth >= self.max_depth:
node.threshold = max(y_classes, key=y.tolist().count)
return node
# 當決策樹葉結點樣本數達到最小樣本數限制時返回樣本中分類佔比最大的分類
if self.min_samples_leaf is not None and len(y) <= self.min_samples_leaf:
node.threshold = max(y_classes, key=y.tolist().count)
return node
min_gini_index = np.inf
min_middle = None
min_feature = None
# 遍歷所有特徵,獲取基尼指數最小的特徵
for i in range(X.shape[1]):
# 計算特徵的基尼指數
gini_index, middle = self.calcGiniIndex(X[:,i], y, y_classes)
if min_gini_index > gini_index:
min_gini_index = gini_index
min_middle = middle
min_feature = i
# 基尼指數最小的特徵
node.feature = min_feature
# 臨界值
node.threshold = min_middle
# 基尼指數
node.gini_index = min_gini_index
X_lt = X[:,min_feature] < min_middle
X_gt = X[:,min_feature] > min_middle
# 遞迴處理左集合
node.left = self.buildNode(X[X_lt,:], y[X_lt], depth + 1)
# 遞迴處理右集合
node.right = self.buildNode(X[X_gt,:], y[X_gt], depth + 1)
return node
def calcMiddle(self, x):
"""
計算連續型特徵的倆倆平均值
"""
middle = []
if len(x) == 0:
return np.array(middle)
start = x[0]
for i in range(len(x) - 1):
if x[i] == x[i + 1]:
continue
middle.append((start + x[i + 1]) / 2)
start = x[i + 1]
return np.array(middle)
def calcGiniIndex(self, x, y, y_classes):
"""
計算基尼指數
"""
x_sort = np.sort(x)
middle = self.calcMiddle(x_sort)
min_middle = np.inf
min_gini_index = np.inf
for i in range(len(middle)):
y_gt = y[x > middle[i]]
y_lt = y[x < middle[i]]
gini_gt = self.calcGini(y_gt, y_classes)
gini_lt = self.calcGini(y_lt, y_classes)
gini_index = gini_gt * len(y_gt) / len(x) + gini_lt * len(y_lt) / len(x)
if min_gini_index > gini_index:
min_gini_index = gini_index
min_middle = middle[i]
return min_gini_index, min_middle
def calcGini(self, y, y_classes):
"""
計算基尼值
"""
gini = 1
for j in range(len(y_classes)):
p = len(y[y == y_classes[j]])/ len(y)
gini = gini - p * p
return gini
def predict(self, X):
"""
分類決策樹預測
"""
y = np.zeros(X.shape[0])
self.checkNode(X, y, self.root)
return y
def checkNode(self, X, y, node, cond = None):
"""
通過分類決策樹結點判斷分類
"""
if node.left is None and node.right is None:
return node.threshold
X_lt = X[:,node.feature] < node.threshold
if cond is not None:
X_lt = X_lt & cond
lt = self.checkNode(X, y, node.left, X_lt)
if lt is not None:
y[X_lt] = lt
X_gt = X[:,node.feature] > node.threshold
if cond is not None:
X_gt = X_gt & cond
gt = self.checkNode(X, y, node.right, X_gt)
if gt is not None:
y[X_gt] = gt
使用 Python 實現基於均方差的決策樹迴歸:
import numpy as np
class RegressorNode:
"""
迴歸決策樹中的結點
"""
def __init__(self, feature=None, threshold=None, mse=None, left=None, right=None):
# 結點劃分的特徵下標
self.feature = feature
# 結點劃分的臨界值,當結點為葉子結點時為分類值
self.threshold = threshold
# 結點的均方差值
self.mse = mse
# 左結點
self.left = left
# 右結點
self.right = right
class RegressorTree:
"""
迴歸決策樹
"""
def __init__(self, max_depth = None, min_samples_leaf = None):
# 決策樹最大深度
self.max_depth = max_depth
# 決策樹葉結點最小樣本數
self.min_samples_leaf = min_samples_leaf
def fit(self, X, y):
"""
迴歸決策樹擬合
"""
self.root = self.buildNode(X, y, 0)
return self
def buildNode(self, X, y, depth):
"""
構建迴歸決策樹結點
"""
node = RegressorNode()
# 當沒有樣本時直接返回
if len(y) == 0:
return node
y_classes = np.unique(y)
# 當樣本中只存在一種分類時直接返回該分類
if len(y_classes) == 1:
node.threshold = y_classes[0]
return node
# 當決策樹深度達到最大深度限制時返回樣本中分類佔比最大的分類
if self.max_depth is not None and depth >= self.max_depth:
node.threshold = np.average(y)
return node
# 當決策樹葉結點樣本數達到最小樣本數限制時返回樣本中分類佔比最大的分類
if self.min_samples_leaf is not None and len(y) <= self.min_samples_leaf:
node.threshold = np.average(y)
return node
min_mse = np.inf
min_middle = None
min_feature = None
# 遍歷所有特徵,獲取均方差最小的特徵
for i in range(X.shape[1]):
# 計算特徵的均方差
mse, middle = self.calcMse(X[:,i], y)
if min_mse > mse:
min_mse = mse
min_middle = middle
min_feature = i
# 均方差最小的特徵
node.feature = min_feature
# 臨界值
node.threshold = min_middle
# 均方差
node.mse = min_mse
X_lt = X[:,min_feature] < min_middle
X_gt = X[:,min_feature] > min_middle
# 遞迴處理左集合
node.left = self.buildNode(X[X_lt,:], y[X_lt], depth + 1)
# 遞迴處理右集合
node.right = self.buildNode(X[X_gt,:], y[X_gt], depth + 1)
return node
def calcMiddle(self, x):
"""
計算連續型特徵的倆倆平均值
"""
middle = []
if len(x) == 0:
return np.array(middle)
start = x[0]
for i in range(len(x) - 1):
if x[i] == x[i + 1]:
continue
middle.append((start + x[i + 1]) / 2)
start = x[i + 1]
return np.array(middle)
def calcMse(self, x, y):
"""
計算均方差
"""
x_sort = np.sort(x)
middle = self.calcMiddle(x_sort)
min_middle = np.inf
min_mse = np.inf
for i in range(len(middle)):
y_gt = y[x > middle[i]]
y_lt = y[x < middle[i]]
avg_gt = np.average(y_gt)
avg_lt = np.average(y_lt)
mse = np.sum((y_lt - avg_lt) ** 2) + np.sum((y_gt - avg_gt) ** 2)
if min_mse > mse:
min_mse = mse
min_middle = middle[i]
return min_mse, min_middle
def predict(self, X):
"""
迴歸決策樹預測
"""
y = np.zeros(X.shape[0])
self.checkNode(X, y, self.root)
return y
def checkNode(self, X, y, node, cond = None):
"""
通過迴歸決策樹結點判斷分類
"""
if node.left is None and node.right is None:
return node.threshold
X_lt = X[:,node.feature] < node.threshold
if cond is not None:
X_lt = X_lt & cond
lt = self.checkNode(X, y, node.left, X_lt)
if lt is not None:
y[X_lt] = lt
X_gt = X[:,node.feature] > node.threshold
if cond is not None:
X_gt = X_gt & cond
gt = self.checkNode(X, y, node.right, X_gt)
if gt is not None:
y[X_gt] = gt
六、第三方庫實現
scikit-learn2 決策樹分類實現
from sklearn import tree
# 決策樹分類
clf = tree.DecisionTreeClassifier()
# 擬合資料
clf = clf.fit(X, y)
scikit-learn3 決策樹迴歸實現
from sklearn import tree
# 決策樹迴歸
clf = tree.DecisionTreeRegressor()
# 擬合資料
clf = clf.fit(X, y)
七、動畫演示
圖7-1展示了一顆未進行正則化的決策樹的分類結果,圖7-2展示了一顆正則化後的決策樹(max_depth = 3, min_samples_leaf = 5)的分類結果
<center>圖7-1</center>
<center>圖7-2</center>
圖7-3展示了一顆未進行正則化的決策樹的迴歸結果,圖7-4展示了一顆正則化後的決策樹(max_depth = 3, min_samples_leaf = 5)的迴歸結果
<center>圖7-3</center>
<center>圖7-4</center>
可以看到未進行正則化的決策樹對訓練資料集明顯過擬合,進行正則化後的決策樹的情況相對好一些。
八、思維導圖
<center>圖8-1</center>
九、參考文獻
完整演示請點選這裡
注:本文力求準確並通俗易懂,但由於筆者也是初學者,水平有限,如文中存在錯誤或遺漏之處,懇請讀者通過留言的方式批評指正
本文首發於——AI導圖,歡迎關注