多叉分類樹
下面實現的分類樹只限於特征是離散變量,而連續變量不能處理。另外,西瓜書介紹的缺失值的處理、多變量處理均未實現。下面實現的樹有一個共同的特點,它的分支依據都是一個具體的特征取值,且每次特征選擇之后都要刪除特征。
一、python實現
我使用python的類實現多分叉決策樹,包括決策樹的訓練和預測兩部分。
1.1樹的結構
使用python的字典(dict)作為樹的結點,字典的嵌套形成樹,格式如下
{'#':feature_name,'feature_value':{}} #樹的結點
#特征名字為0,取值為0的分支
{'#': 0, 0: 0, 1: {'#': 1, 0: 0, 1: 1}} #例子
1.2 種樹
1.2.1 種樹流程
建樹的過程就是迭代選擇划分的特征,每一次迭代選擇一個特征進行划分。決策樹的訓練一般遵循以下兩個步驟:
- 特征選擇
- 進入下一次遞歸(給子集進行特征選擇)
其中,迭代返回的情況有
- 類別值都一樣,返回該類別
- 特征值都一樣,返回類別頻數最大的哪一類
1.2.2 特征選擇指標
特征選擇就是選擇“純度”(混亂程度越低)最大的特征。前面提到,信息增益、信息增益率、基尼指數都可以用於特征選擇。接下來根據它們的公式,可以依次寫出相應的函數,用於選擇純度最大的特征。
-
權值
下面的公式中的\(p_k\)(概率)或者\(\frac{|D^v|}{|D|}\)(權值)都可以用這個公式計算。其中注意的是兩個參數都是數組類型。
def cal_weight(y,w=None):
'''計算離散變量的權值\概率
:param y: 數組,arr
:param w: 樣本權值,arr
:return:
'''
unique_val = set(y) #用數組還是字典存儲結果?用生成器
if w is None:
m = len(y)
for v in unique_val:
yield v,sum(v==y)/m #用生成器返回結果:取值,權值\概率
else:
sum_ = sum(w)
for v in unique_val:
yield v,sum(w[y==v])/sum_ #用生成器返回結果:取值,權值\概率
yield None,0 #y為空的情況
-
信息熵
這里的信息熵不直接作為特征選擇指標,而是作為信息增益的一部分
# 計算信息熵
def Ent(y,w): #計算信息熵只需要用到數據集D中的因變量y
'''
:param y:因變量y,shpae =(m);arr類型
:param w: 樣本權值,arr
:return:
'''
ent = 0
for v,p in cal_weight(y,w):
ent -= p*np.log2(p)
return ent
- 信息增益(ID3)
def Gain(x_i,y,ent,w):
'''
:param x_i:第i個特征(屬性),1*m
:param w: 樣本權值,arr
:return:
'''
gain = ent #信息增益
for v,p in cal_weight(x_i,w):
index = x_i == v #取值為v的索引
w_ = w if w is None else w[index]
gain -= p**Ent(y[index],w_)
return gain
- 信息增益率(C4.5)
#第i個特征的信息增益率
def Gain_Radio(x_i,y,ent,w ):
'''
:param x_i:第i個特征(屬性),1*m
:return:
'''
gain = ent #信息增益
iv = 1e-9 #固有值,平滑處理
for v,p in cal_weight(x_i,w):
index = x_i == v # 取值為v的索引
w_ = w if w is None else w[index]
gain -= p**Ent(y[index],w_)
iv -=p*np.log2(p)
return gain/iv
-
Gini(基尼值)
基尼值也不直接作為特征選擇指標,而是作為基尼指數的一部分
#第i個特征的基尼值
def Gini(y,w):
p_2 = 0
for v,p in cal_weight(y,w):
p_2 += p**2
return 1- p_2
- 基尼指數
#第i個特征的基尼指數
def Gini_index(x_i,y,w):
gini_index = 0
for v,p in cal_weight(x_i,w):
index = x_i == v # 取值為v的索引
w_ = w if w is None else w[index]
gini_index += p**Gini(y[index],w_)
return gini_index
1.2.3 生成樹(種樹)
下面是決策樹的整體結構。接下來解釋構造函數三個參數的作用:
- criterion:選擇特征選擇方法
- splitter:選擇是否隨機特征選擇
- weight:樣本權重
其中splitter、weight有何作用?答案是用來種森林。
若splitter選擇'random',可以用來寫ExtraTree(極度隨機森林)
若指定weight,可以用來寫AdaBoost(...森林)
#多叉分類樹
class ClassifyTree_:
def __init__(self,criterion="gini",splitter='best',weight=None):
self.criterion = criterion
self.weight = weight
self.splitter = splitter
#----------特征選擇方法-----------------
def id3(self,X,y,weight): #criterion="id3",splitter='best'
def c45(self,X,y,weight): #criterion="C45",splitter='best'
def gini(self,X,y,weight): #criterion="gini",splitter='best'
def rand_(self,X,y,weight): #splitter='random'
#----------種樹-------------------------
def build_(self,X,y,feat_lst,criterion,weight=None): #這里需要傳入特征列表,因為X改變了
def fit(self, X, y,weight=None):
# 四種不同的樹
self.weight = weight
if self.splitter == 'best':
if self.criterion == 'id3':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.id3, weight)
elif self.criterion == 'c45':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.c45, weight)
elif self.criterion == 'gini':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.cart, weight)
else:
raise ('gini/c45/id3')
else:
self.tree = self.build_(X, y, list(range(X.shape[1])), self.rand_, weight)
return self
#----------預測-------------------------
def predict(self, X):
1.3 例子
下面實現的分類樹只限於特征是離散變量,而連續變量不能處理。另外,西瓜書介紹的缺失值的處理、多變量處理均未實現。閱讀這些例子可以輕松理解上面的建樹流程。注意,下面的例子都是簡易版本的決策樹,而非完整版。
1.3.1 ID3決策樹
- 使用信息增益划分數據集
# 使用id3拿到最佳特征的索引
def id3(self,X,y,weight):
best_Index = -1
best_gain = -np.inf
ent = Ent(y,self.weight)
for i in range(X.shape[1]):
gain = Gain(X[:,i],y,ent,weight)
if gain > best_gain:
best_gain = gain
best_Index = i #信息增益最大的特征
return best_Index
這個建樹函數需要注意的兩個點:
為何要傳入\(feat\_lst\)(各個特征的名字)? 因為每次划分后,特征會被刪除掉。
注意2個步驟和3個退出條件
def build_(self,X,y,feat_lst,criterion,weight=None): #這里需要傳入特征列表,因為X改變了
'''
:param X:
:param y:
:param feat_lst:特征名字的列表
:return:
'''
m,n = X.shape #樣本,特征數量
# if m==0: return # 返回1:沒有樣本了,退出;;會出現這種情況嗎?
if len(set(y)) == 1:return y[0] #返回2:類別值都一樣
# 1.特征選擇
if n == 1:
node = {'#': feat_lst[0]} # 結點,存儲特征的索引
x = X[:, 0]
for val in set(x): # 該特征所有的取值
node[val] = cal_mode(x[x==val]) #取眾數
else:
best_Index = criterion(X, y, weight)
splitVal = set(X[:,best_Index]) #該特征所有的取值
if len(splitVal)==1 :return cal_mode(y) #返回3:特征值都一樣,返回頻數最大的類別
else:
node = {'#':feat_lst[best_Index] } #結點,存儲特征的索引
index = list(range(n))
index.pop(best_Index) # 需要划分的特征index
feat_l=feat_lst[:] #避免影響,前面的
feat_l.pop(best_Index)
# 2.划分數據集,遞歸調用種子樹
for val in splitVal:
i_sample = X[:, best_Index] == val #子數據集
weight_ = weight if weight is None else weight[i_sample]
node[val] = self.build_(X[i_sample][:, index], y[i_sample], feat_l,criterion,weight_)
return node
- 訓練的函數入口
def fit(self, X, y,weight=None):
# 建樹
self.weight = weight #保存樣本權重
if self.splitter == 'best':
if self.criterion == 'id3':
# 這里用索引來代替特征的名字 list(range(X.shape[1])):索引
self.tree = self.build_(X, y, list(range(X.shape[1])), self.id3, weight)
- 預測函數
# 分不同數據類型進行調用;二維數組或者一個向量(樣本)
def predict(self, X):
if len(X.shape) > 1: # 二維數組
rst = np.zeros(X.shape[0])#.astype(objecT),可以存放字符串
for i,x in enumerate(X):
rst[i] = self.predict_(x)
elif len(X) == 0:
rst = np.inf
else:
rst = self.predict_(X)
return rst
# 真正開始預測
def predict_(self,x):
tree = self.tree
while True:
if isinstance(tree,dict):
key = tree['#'] #樹的名字
else:
return tree
try:
tree = tree[x[key]] #根據取值進入下一級
except:
return np.inf
ID3決策樹使用選擇信息增益最大的特征進行划分。稍微將特征選擇的標准改變,可得C4.5決策樹。在信息增益高於平均水平的特征中選擇信息增益率最大的。同樣地,將指標改成基尼指數,也可以得到...決策樹。
二、測試
2.1 可跑性測試
一般而已,當你花費九牛二虎之力終於把一顆樹的代碼擼完之后,都會遭到跑不動沉痛打擊。所以,我們先拿簡單的數據集來測試。
def valid():
'''樹能不能跑'''
dataSet = np.array([[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']])
X = dataSet[:,:-1]
y = dataSet[:, -1]
m = ClassifyTree_()
m.fit(X, y) #訓練
print(m.predict(np.array(['1','1']))) #預測
return m.tree
if __name__ == '__main__':
a = valid()
print(a)
結果如下

三、完整代碼
下面可以通過傳入不同參數選擇不同的樹。
import numpy as np
from utils import cal_mode,Gini_index,Ent,Gain,Gain_Radio
#多叉分類樹
class ClassifyTree_:
def __init__(self,criterion="gini",splitter='best',weight=None):
self.criterion = criterion
self.weight = weight
self.splitter = splitter
def id3(self,X,y,weight):
best_Index = -1
best_gain = -np.inf
ent = Ent(y,self.weight)
for i in range(X.shape[1]):
gain = Gain(X[:,i],y,ent,weight)
if gain > best_gain:
best_gain = gain
best_Index = i #信息增益最大的特征
return best_Index
def c45(self,X,y,weight): #這里需要傳入特征列表,因為X改變了
'''建樹'''
# 特征選擇
n = X.shape[1]
gain_arr = np.zeros(n) # 增益
ent = Ent(y,self.weight)
for i in range(n): # 特征數量
gain_arr[i] = Gain(X[:, i], y, ent,weight)
m_gain = np.mean(gain_arr) # 平均增益
best_Index = -1
best_gain_radio = -np.inf
for i in range(n): # 對每個特征
if gain_arr[i] > m_gain:
gain_radio = Gain_Radio(X[:, i], y, ent,weight)
if gain_radio > best_gain_radio:
best_gain_radio = gain_radio
best_Index = i
return best_Index
def gini(self,X,y,weight):
'''建樹'''
# 特征選擇
best_Index = -1
best_gini_index = np.inf
for i in range(X.shape[1]):
gini_index = Gini_index(X[:, i], y,weight)
if gini_index < best_gini_index:
best_gini_index = gini_index
best_Index = i # 基尼指數最小的特征
return best_Index
def rand_(self,X,y,weight):
return np.random.choice(X.shape[1])
def build_(self,X,y,feat_lst,criterion,weight=None): #這里需要傳入特征列表,因為X改變了
'''
:param X:
:param y:
:param feat_lst:特征名字的列表
:return:
'''
# 特征選擇
m,n = X.shape #樣本,特征數量
# if m==0: return # 沒有樣本了,退出;;;會出現這種情況嗎
if len(set(y)) == 1:return y[0] #類別值都一樣
if n == 1:
node = {'#': feat_lst[0]} # 結點,存儲特征的索引
x = X[:, 0]
for val in set(x): # 該特征所有的取值
node[val] = cal_mode(x[x==val]) #取眾數
else:
best_Index = criterion(X, y, weight)
splitVal = set(X[:,best_Index]) #該特征所有的取值
if len(splitVal)==1 :return cal_mode(y) #特征值都一樣,返回頻數最大的類別
else:
node = {'#':feat_lst[best_Index] } #結點,存儲特征的索引
index = list(range(n))
index.pop(best_Index) # 需要划分的特征index
feat_l=feat_lst[:] #避免影響,前面的
feat_l.pop(best_Index)
for val in splitVal:
i_sample = X[:, best_Index] == val #子數據集
weight_ = weight if weight is None else weight[i_sample]
node[val] = self.build_(X[i_sample][:, index], y[i_sample], feat_l,criterion,weight_)
return node
def fit(self, X, y,weight=None):
# 建樹
self.weight = weight
if self.splitter == 'best':
if self.criterion == 'id3':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.id3, weight)
elif self.criterion == 'c45':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.c45, weight)
elif self.criterion == 'gini':
self.tree = self.build_(X, y, list(range(X.shape[1])), self.gini, weight)
else:
raise ('gini/c45/id3')
else:
self.tree = self.build_(X, y, list(range(X.shape[1])), self.rand_, weight)
return self
# 分不同數據類型進行調用;二維數組或者一個向量(樣本)
def predict(self, X):
if len(X.shape) > 1: # 二維數組
rst = np.zeros(X.shape[0])#.astype(objecT),可以存放字符串
for i,x in enumerate(X):
rst[i] = self.predict_(x)
elif len(X) == 0:
rst = np.inf
else:
rst = self.predict_(X)
return rst
# 真正開始預測
def predict_(self,x):
tree = self.tree
while True:
if isinstance(tree,dict):
key = tree['#'] #樹的名字
else:
return tree
try:
tree = tree[x[key]] #根據取值進入下一級
except:
return np.inf
def valid():
'''樹能不能跑'''
dataSet = np.array([[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']])
X = dataSet[:,:-1]
y = dataSet[:, -1]
m = ClassifyTree_()
m.fit(X, y) #訓練
print('預測結果',m.predict(np.array(['1','1']))) #預測
return m.tree
if __name__ == '__main__':
a = valid()
print('訓練出來的樹:',a)
