算法原理
決策樹是一類經典的機器學習方法,既可以用於分類任務,也可以用於迴歸。分類和迴歸對應的分別是分類樹和迴歸樹,本文將以最常見的一類決策樹——ID3分類樹爲例,講解模型的原理以及程序實現。
模型
已知一組有n個樣本的訓練集
其中,每個樣本都有個特徵(爲了簡便起見,假設樣本的特徵都是離散值)。現在的目的是希望通過對訓練集進行訓練。之後往模型中代入新的樣本,得到預測的分類類別。
原理
在講原理之前先通過一個例子引入,假設現在是晚上,你在考慮是否要玩遊戲,這時可能要先判斷現在是幾點,是否已經很晚了,如果很晚則不玩遊戲,若是現在還早,那可能還要判斷明天需要上交的作業寫好了沒有,還沒寫好則要抓緊時間寫,如果寫好了,就可以去玩遊戲了。這個例子的圖示可以表示爲
決策樹依次通過樣本的特徵自上而下地對樣本進行分類,根據某一個特徵的不同,可以將樣本分成若干組,對於樣本類別全都相同的組則停止分類,對於樣本類別不全相同的組,則需要根據剩餘的特徵再進行分類,直至最後得到的每個組裏的樣本類別全相同。
爲了理解決策樹的構建過程,需要先弄清楚幾個問題
樹的組成
決策樹包含節點和分支。
圖上的方框即代表節點,它是根據特徵進行分類得到的數據組。其中,節點包括根節點,中間節點和葉子節點。根節點是指最初始的節點,即還沒有進行任何分類的節點。葉子節點是指不用再進行分類的節點。不是根節點和葉子節點的節點便是中間節點。
分支是指由節點引出的若干條細支,每條分支都代表一個分類過程。
哪些特徵需要優先選擇
對應到決策樹中的節點,由於決策樹是自上而下構建的,並且每次只通過一個特徵進行分類。而樣本集中的特徵有很多,現在需要考慮的是要優先選擇哪一個特徵進行分類?按正常情況來講當然是分類後得到的各組數據的純度越高越好,這裏的純度體現在每組數據中各類別的分佈情況上。當每組數據中只有一種類別,則純度是最高的,當不止有一種類別,則純度會較低一些。衡量這種純度的方法有很多,本文介紹的是ID3算法,它是通過計算信息增益來選擇特徵的。
信息增益=分類前信息熵-分類後的加權信息熵
其中,是分類前的信息熵,指分類前的樣本總數,是分類後的第i組數據的樣本總數,是指分類後的第i組的信息熵。
爲了使得純度更高,我們希望分類後的信息熵更小,因此要選取信息增益最大的那個特徵。按照上述方法對類別數超過一的節點即數據集進行分類,直至得到的分組數據集中類別都只有一種。
分類樹與迴歸樹的區別
最明顯的不同就是分類樹輸出的是樣本的類別,屬於離散值,而回歸樹輸出的是實數,是連續值。另外,樣本的特徵也有離散值和連續值的差別,如果特徵是離散值,則在選取特徵時可以用普通的ID3、C4.5和Gini方法。如果特徵是連續值,則需要將該特徵的數值按大小進行排序,然後分別取這些數值作爲閾值將數據分成兩類,結合ID3、C4.5或Gini方法選出最優的特徵。
程序實現
各函數
計算信息熵
def cal_entropy(y):
"""信息熵計算
參數
-------
y:類別編號 類型:narray, shape:{n_samples}
返回
----
e:信息熵 類型:float
"""
count = np.array(pd.Series(y).value_counts())
p = count/count.sum()
return -np.sum(np.log2(p)*p)
選取最優的特徵
def choose_features_ID3(X, y):
"""選擇特徵(單個)
參數
------
X:特徵,類型:ndarray,shape:{n_samples, n_features}
y: 類別編號 類型:narray, shape:{n_samples}
返回
-----
min_fea_index:選出的特徵,類型:integer
entropy:信息增益,類型:float
"""
n_samples, n_features = X.shape
fea_index = 0
max_entropy = 0
pre_y_entropy = cal_entropy(y)
for i in range(n_features):
entropy_sum = 0
row_value = X[:,i]
for value in set(row_value):
bools = row_value==value
entropy_sum += np.sum(bools)/n_samples * cal_entropy(y[bools])
entropy = pre_y_entropy-entropy_sum
if entropy>max_entropy:
max_entropy = entropy
fea_index = i
return fea_index,entropy
構建ID3決策樹
def tree_ID3(X, y, X_name):
"""構建決策樹,採用ID3,無剪枝操作
參數
------
X:特徵,類型:ndarray,shape:{n_samples, n_features}
y: 類別編號,類型:ndarray,shape:{n_samples}
X_name: 特徵名,類型:ndarray,shape:{n_samples}
"""
if not len(X):return
if cal_entropy(y)==0:return y[0]
n_samples, n_features = X.shape
index = choose_features_ID3(X, y)[0]
dic = {X_name[index]:{}}
remove_fea = X[:, index]
for fea in set(remove_fea):
row_bool = remove_fea==fea # 留下的行索引
col_bool = np.arange(n_features)!=index # 留下的列索引
dic[X_name[index]][fea] = tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
return dic
實例化演示
以西瓜數據集爲例
dataSet = np.array([
# 1
['青綠', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
# 2
['烏黑', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', '好瓜'],
# 3
['烏黑', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
# 4
['青綠', '蜷縮', '沉悶', '清晰', '凹陷', '硬滑', '好瓜'],
# 5
['淺白', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', '好瓜'],
# 6
['青綠', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', '好瓜'],
# 7
['烏黑', '稍蜷', '濁響', '稍糊', '稍凹', '軟粘', '好瓜'],
# 8
['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '硬滑', '好瓜'],
# ----------------------------------------------------
# 9
['烏黑', '稍蜷', '沉悶', '稍糊', '稍凹', '硬滑', '壞瓜'],
# 10
['青綠', '硬挺', '清脆', '清晰', '平坦', '軟粘', '壞瓜'],
# 11
['淺白', '硬挺', '清脆', '模糊', '平坦', '硬滑', '壞瓜'],
# 12
['淺白', '蜷縮', '濁響', '模糊', '平坦', '軟粘', '壞瓜'],
# 13
['青綠', '稍蜷', '濁響', '稍糊', '凹陷', '硬滑', '壞瓜'],
# 14
['淺白', '稍蜷', '沉悶', '稍糊', '凹陷', '硬滑', '壞瓜'],
# 15
['烏黑', '稍蜷', '濁響', '清晰', '稍凹', '軟粘', '壞瓜'],
# 16
['淺白', '蜷縮', '濁響', '模糊', '平坦', '硬滑', '壞瓜'],
# 17
['青綠', '蜷縮', '沉悶', '稍糊', '稍凹', '硬滑', '壞瓜']
])
X = dataSet[:,:-1]
y = dataSet[:,-1]
X_name = np.array(['色澤','根蒂','敲聲','紋理','臍部','觸感'])
tree_ID3(X,y,X_name)
out
{'紋理': {'稍糊': {'觸感': {'硬滑': '壞瓜', '軟粘': '好瓜'}},
'清晰': {'根蒂': {'稍蜷': {'色澤': {'青綠': '好瓜',
'烏黑': {'觸感': {'硬滑': '好瓜', '軟粘': '壞瓜'}}}},
'硬挺': '壞瓜',
'蜷縮': '好瓜'}},
'模糊': '壞瓜'}}
封裝成一個類
增加訓練函數fit和預測函數predict、check,其中,check函數是對一個樣本進行預測,predict函數整合了多個樣本。
class Tree_ID3:
def __init__(self):
pass
def cal_entropy(self, y):
"""信息熵計算
參數
-------
y:類別, 類型:narray, shape:{n_samples}
返回
----
e:信息熵, 類型:float
"""
count = np.array(pd.Series(y).value_counts())
# 每個類別的概率
p = count/count.sum()
# 信息熵
return -np.sum(np.log2(p)*p)
def choose_features_ID3(self, X, y):
"""選擇特徵(單個)
參數
------
X:特徵, 類型:ndarray, shape:{n_samples, n_features}
y:類別, 類型:ndarray, shape:{n_samples}
返回
-----
fea_index:選出的特徵, 類型:integer
entropy:信息增益, 類型:float
"""
n_samples, n_features = X.shape
# 最優特徵的索引
fea_index = 0
# 最大的信息增益
max_entropy = 0
# 分類前標籤y的信息熵
pre_y_entropy = self.cal_entropy(y)
for i in range(n_features):
# 初始化分類後的加權信息熵
entropy_sum = 0
row_value = X[:,i]
for value in set(row_value):
# 選中的樣本索引
bools = row_value==value
entropy_sum += np.sum(bools)/n_samples * self.cal_entropy(y[bools])
# 當前信息增益
entropy = pre_y_entropy-entropy_sum
if entropy>max_entropy:
max_entropy = entropy
fea_index = i
return fea_index,entropy
def tree_ID3(self, X, y, X_name):
"""構建決策樹
參數
------
X:特徵, 類型:ndarray, shape:{n_samples, n_features}
y:類別編號, 類型:ndarray, shape:{n_samples}
X_name:特徵名, 類型:ndarray, shape:{n_samples}
返回
-----
dic:決策樹, 類型:dict
"""
if not len(X):return
# 只剩一類,返回
if self.cal_entropy(y)==0:return y[0]
n_samples, n_features = X.shape
index = self.choose_features_ID3(X, y)[0]
# 決策樹構建
dic = {X_name[index]:{}}
remove_fea = X[:, index]
for fea in set(remove_fea):
# 剩下的行索引
row_bool = remove_fea==fea
# 剩下的列索引
col_bool = np.arange(n_features)!=index
# 遞歸
dic[X_name[index]][fea] = self.tree_ID3(X[row_bool][:,col_bool], y[row_bool], X_name[col_bool])
return dic
def check(self, tree, X, X_name):
"""預測
"""
if not len(tree) or not len(X):return
cur_fea_name = list(tree.keys())[0]
cur_fea_index = np.where(X_name==cur_fea_name)[0][0]
if X[cur_fea_index] not in tree[cur_fea_name].keys():return
if tree[cur_fea_name][X[cur_fea_index]] in self.y_name:
return tree[cur_fea_name][X[cur_fea_index]]
else:
bools = np.arange(len(X))!=cur_fea_index
return self.check(tree[cur_fea_name][X[cur_fea_index]], X[bools], X_name[bools])
def fit(self, X, y, X_name):
self.X_name = X_name
self.y_name = list(set(y))
self.tree = self.tree_ID3(X, y, X_name)
def predict(self, X):
res = []
for i in range(len(X)):
res.append(self.check(self.tree, X[i], self.X_name))
return np.array(res)
演示
clf = Tree_ID3()
clf.fit(X, y, X_name)
predict_y = clf.predict(X)
sum(predict_y==y)==len(y)
以上便是ID3分類樹的代碼實現,如有不對不妥之處,歡迎交流批評改正。
參考資料:
周志華《機器學習》