1. 決策樹python源碼實現--多叉分類樹


多叉分類樹

​ 下面實現的分類樹只限於特征是離散變量,而連續變量不能處理。另外,西瓜書介紹的缺失值的處理多變量處理均未實現。下面實現的樹有一個共同的特點,它的分支依據都是一個具體的特征取值,且每次特征選擇之后都要刪除特征

一、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. 進入下一次遞歸(給子集進行特征選擇)

其中,迭代返回的情況有

  • 類別值都一樣,返回該類別
  • 特征值都一樣,返回類別頻數最大的哪一類

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為空的情況
  • 信息熵

    這里的信息熵不直接作為特征選擇指標,而是作為信息增益的一部分

\[Ent(D)=-\sum_{k=1}^np_k\log_2{p_k} \]

# 計算信息熵
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)

\[Gain(D,x_i)=Ent(D)-\sum_{i=1}^v\frac{|D^v|}{|D|}Ent(D^v)\\ 其中|D^v|是所有取值為v的樣本數量 \]

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)

\[Gain\_radio(D,x_i)=\frac{Gain(D,x_i)}{IV(x_i)}\\ 其中屬性x_i的“固有值”\\ IV=-\sum_i^v\frac{|D_v|}{|D|}\log_2\frac{|D_v|}{|D|} \]

#第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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM