決策樹:原理以及python實現


Table of Contents

決策樹概述

決策樹的決策方式

  如下圖所示,決策樹的決策過程本質上是一系列的if/then語句,通過學習到的規則來做出決策。在下圖的郵件分類應用中,我們的規則是如果郵件來自myEmployer.com那么郵件就被分類為“無聊的時候再讀”,否則再判斷郵件內容是否包含“曲棍球”,如果是,那么就是來自朋友的郵件,被分類為“立刻閱讀”,否則被分類為“垃圾郵件”。

  顯然,決策樹的決策規則具有一個重要性質:互斥且完備。這意味着,對於每一個樣本,有且只有一條路徑使其從根節點走到某個葉節點。同時,由於對樣本特征不斷的進行一系列的條件\(X_i\)的判斷,決策樹也可以理解為對\(P(y_i|X_i)\)的條件概率的求解。比如下面這個例子可以理解為,在郵件來自myEmployer.com時,郵件屬於“無聊的時候再讀”的概率是100%。

image

決策樹的規則學習過程

  為了構造決策樹,算法遍歷所有可能詢問的問題,找出對於目標變量來說信息量最大的一個,將數據集分為兩部分,重復此過程直到結束,其原理如下:

輸入:訓練集\(D=\{(x_1,y_1),(x_2,y_2),...,(x_m,y_m)\}\)

屬性集\(A=\{a_1,a_2,a_3,...,a_n\}\)

createBranch()方法:

檢測數據集\(D\)中的所有數據的分類標簽\(A\)是否相同:

If so return 類標簽
Else:
    從A中尋找最優划分特征
    划分數據集
    創建分支節點
        for 每個划分的子集
            調用函數 createBranch(創建分支的函數)並增加返回結果到分支節點中
    return 分支節點

特征選擇

  上一節說到,決策樹在划分節點時,選擇一個最佳特征將樣本數據划分到不同的節點中,那么,該如何選擇最優特征呢?

信息熵

  第二節中的算法中,某個節點停止划分成為葉節點的條件是,節點內所有樣本均屬於同一類別。也就是節點是“純凈的”。因此,在選擇特征進行划分時,使得節點越純凈的特征就是越好的特征。我們使用信息熵(Entropy)來衡量一組樣本的“純凈度”。

  對於離散型隨機變量X,其分布為:

\[P(X=X_i)= p_i,i = 1,2,3...k \]

則其信息熵為

\[H(X) =- \sum\limits_{i=1}^kp_ilnp_i,\sum\limits_{i=1}^kp_i=1 \]

那么為什么信息熵能夠衡量純凈度呢?

二分類

  首先,考慮二分類,即

\[p_1+p_2=1 \]

\[H=-(p_1lnp_1+p_2lnp_2) \]

則有

\[\frac{\partial H}{\partial p_i}=-lnp_i-1-\frac{dp_j}{dp_i}lnp_j-\frac{dp_j}{dp_i}=ln(\frac 1{p_i}-1) \]

\(\frac12\leq p_i\leq1\)時,\(H'(p_i)\leq0\)\(H\)遞減。

\(0\leq p_i\leq\frac12\)時,\(H'(p_i)\leq0\)\(H\)遞增。

\(p_1=\frac 12\)時,信息熵有最大值。

  如下圖所示,也就是說,信息熵越大,兩個類別的樣本數量也就越接近。舉一個極端的例子,A類樣本50%,B類樣本50%的節點純凈程度顯然不如A類100%,B類0%的節點純凈度。也就是說,可以認為,信息熵越大,節點的“純凈度”就越低。因此,對於二分類,信息熵越小越好。

import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

x = np.linspace(0,1)
y = -x*np.log(x)-(1-x)*np.log(1-x)
plt.xlabel('pi')
plt.ylabel('H(pi)')
plt.plot(x,y)
plt.show()


image

多分類

對於多分類,同樣的有這樣一個結論:對於\(H(X) = - \sum\limits_{i=1}^kp_ilnp_i,\sum\limits_{i=1}^kp_i=1\),當\(p_i=\frac 1k\)時,信息熵\(H\)有最大值。

約束條件下的極值很自然的想到使用拉格朗日乘數法證明:

\[F=-\sum\limits_{i=1}^kp_ilnp_i+\lambda(\sum\limits_{i=1}^kp_i-1) \]

\[F'(p_i)=-lnp_i-1+\lambda \]

\[令F'(p_i)=0可得,p_i = e^{\lambda -1} \]

\[由於\sum p_i=1,故p_i=\frac 1k \]

還有一個有意思的證明:http://www.math345.com/blog/article/17

信息增益

  上一節中提到了使用信息熵來衡量葉節點的純凈度,很自然的,在最優特征選擇的時候,可以選擇使得信息熵變得最小的特征作為最優特征。

信息增益(Information Gain):特征\(A\)對於節點數據\(D\)的信息增益

\[G=H(D)-\sum\limits_{i=1}^m\frac{|D_i|}{|D|}H(D_i) \]

  可以看出,由於信息熵的絕對大小只與變量的分布有關。因此,在划分了多個節點之后,使用每個節點的樣本數量占上一級節點樣本數的比例作為權重對信息熵進行加權,然后計算加權信息熵與划分前的信息熵的差,作為信息熵的增量,也就是信息增益。

信息增益比

  使用信息增益作為划分數據集的特征,偏向於選擇類別多的特征。考慮一個極端的二分類問題,假設當前節點有20個樣本,正負樣本各占一半,某個特征\(A\)剛好有20個類別,將其划分為20個純凈的節點,信息增益達到最大。另一個特征\(B\)只將訓練數據划分為2個節點,並且節點不純凈。按照信息增益准則,將選擇特征\(A\)作為划分特征,但是,在預測時,特征\(A\)很可能因為過擬合而泛化性能變低。因此,可使用信息增益比作為划分標准。

\[G_r=\frac G{IV},IV=-\sum\limits_{i=1}^m\frac{|D_i|}{|D|}ln\frac{|D_i|}{|D|} \]

  由於對於

\[H(X) =- \sum\limits_{i=1}^kp_ilnp_i,\sum\limits_{i=1}^kp_i=1 \]

\(p_i=\frac 1k\)時候,有

\[H_{max}=lnk \]

  也就是說,隨着隨機變量\(X\)類別的增加\(H_{max}\)是增加的。因此,可以用信息增益與特征\(A\)的固有值\(IV\)之比來作為划分標准。但是,根據周志華老師的說法,信息增益比會偏好分類類別較少的特征,這應當是由固有值增長的速度較快導致的。

基尼系數

  定義基尼系數

\[Gini=1-\sum\limits_{i=1}^k p_i^2 \]

  基尼系數表示的是在一個節點中,隨機選取兩個樣本點,其類別不同的概率。節點越純凈,概率就越低。如下所示是在二分類的情況下,Gini系數與信息熵的一半(以2為底)的圖像。可以看出,二者十分相似。可以采用第一節中的方法證明,gini系數與信息熵有類似的性質。

x = np.linspace(0,1)
ent = (-x*np.log2(x)-(1-x)*np.log2(1-x))/2
gini = 2*x*(1-x)
plt.xlabel('pi')
plt.ylabel('y')
plt.plot(x,ent,c='y',label='Ent')
plt.plot(x,gini,c='black',label='Gini')
plt.legend()
plt.show()


image

ID3

算法流程

  ID3是經典的機器學習算法,使用信息增益選擇划分特征。其方法是:從根節點開始,計算所有可能的特征的信息增益,然后選擇信息增益最大的特征對節點進行划分。然后遞歸地對每一個節點使用此划分方法,直到信息增益為0或小於閾值,或無可用特征。

  在進行預測時,只需按照已經構建好的樹逐一判斷條件,將待預測樣本分入相應的葉節點中,然后選擇節點中類別較多的類別作為預測類別。

Python實現

  這里實現的是完全生長的決策樹,可以增加閾值\(\epsilon\),當信息增益\(G<\epsilon\)時,即停止生長。

import collections
import numpy as np


class ID3:
    def __init__(self, X, y, feature_name):
        self.features = X
        self.labels = y
        self.data = np.hstack((self.features, self.labels))
        self.feature_label = feature_name
        self.tree = {}

    def calEntropy(self, data):
        '''
        計算信息熵
        :param data:列表等序列
        :return:輸入數據的信息熵
        '''
        entropy = 0
        c = collections.Counter(np.array(data).ravel())
        total = len(data)
        for i in c.values():
            entropy -= i/total * np.log(i/total)
        return entropy

    def splitdata(self, data, col, value):
        '''
        :param data:
        :param col:待划分特征列的數字索引
        :param value:
        :return:返回輸入data的col列等於value的去掉col列的矩陣
        '''
        data_r = data[data[:, col] == value]
        data_r = np.hstack((data_r[:, :col], data_r[:, col+1:]))
        return data_r

    def getBestFeature(self, data):
        '''
        :param data: 形式為[X y]的矩陣
        :return: 最優特征所在列索引
        '''
        entropy_list = []
        numberAll = data.shape[0]
        for col in range(data.shape[1]-1):
            entropy_splited = 0
            for value in np.unique(data[:, col]):
                y_splited = self.splitdata(data, col, value)[:, -1]
                entropy = self.calEntropy(y_splited)
                entropy_splited += len(y_splited)/numberAll*entropy
            entropy_list.append(entropy_splited)
        return entropy_list.index(min(entropy_list))

    def CreateTree(self, data, label):
        '''

        :param data: 形如[X y]的矩陣
        :param feature_label:
        :return: 決策樹字典
        '''
        feature_label = label.copy()
        if len(np.unique(data[:, -1])) == 1:
            return data[0, -1]
        if data.shape[1] == 1:
            return collections.Counter(data[:, -1]).most_common()[0][0]
        bestFeature = self.getBestFeature(data)
        bestFeatureLabel = feature_label[bestFeature]
        treeDict = {bestFeatureLabel: {}}

        del feature_label[bestFeature]
        for value in np.unique(data[:, bestFeature]):
            sub_labels = feature_label[:]
            splited_data = self.splitdata(data, bestFeature, value)
            treeDict[bestFeatureLabel][value] = self.CreateTree(splited_data, sub_labels)
        return treeDict

    def fit(self):
        self.tree = self.CreateTree(self.data, self.feature_label)

    def predict_vec(self, vec, input_tree=None):
        if input_tree==None:
            input_tree = self.tree
        featureIndex = self.feature_label.index(list(input_tree.keys())[0])
        secTree = list(input_tree.values())[0]
        vec_feature_val = str(vec[featureIndex])
        if type(secTree.get(vec_feature_val)) != dict:
            return secTree.get(vec_feature_val)
        else:
            return self.predict_vec(vec, secTree.get(vec_feature_val))

    def predict(self, X):
        out_put=[]
        for i in X:
            out_put.append(self.predict_vec(i))
        return out_put


def main():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing', 'flippers']
    X = np.array(dataSet)[:, :-1]
    y = np.array(dataSet)[:, -1].reshape(-1, 1)
    id3 = ID3(X, y, labels)
    id3.fit()
    print(id3.tree)
    print(id3.predict_vec([1, 0]))
    print(id3.predict(X))


if __name__ == '__main__':
    main()
{'no surfacing': {'0': 'no', '1': {'flippers': {'0': 'no', '1': 'yes'}}}}
no
['yes', 'yes', 'no', 'no', 'no']

小結

  ID3在當時提出了一個新的思路,但是仍然有很多問題。比如,只能處理分類變量,每個特征只能使用一次,信息增益偏好類別多的特征,容易過擬合等。

C4.5

  C4.5是對ID3的改進,李航老師的《統計學習方法》中提到的改進,只有使用了信息增益比來代替信息增益。@劉建平Pinard 老師的博客中,提到了一些其他的優化方法也是C4.5所采用的。總體有以下幾個方面:

連續值的處理

  連續值采用機器學習中常用的方法:連續變量離散化,即對連續值進行分箱操作。決策樹算法中,選定一個閾值,將連續變量分為兩類,大於閾值的屬於類別1,小於閾值的屬於類別0。具體方法如下,對於輸入矩陣\(X_{m\times n}\),某連續特征\(A\)的取值從小到大排列為序列\(a_1,a_2,...,a_m\),考察包括\(m-1\)個划分點的集合

\[T=\{\frac{a_i+a_{i+1}}2|1\leq i\leq m-1\} \]

也就是分別選取兩個相鄰值得中位數作為划分閾值,將特征映射為兩個類別。然后按照前面所說得離散變量的處理方法,選擇信息增益最大的划分方法。

特征選擇

  ID3使用的是前面提到的信息增益作為划分標准,C4.5使用了前面提到的信息增益比。周志華老師書中提到,信息增益比偏好划分類別較少的特征,信息增益偏好類別較多的特征,因此,可以先選取信息增益高於平均值的特征,然后再從中選擇信息增益率最高的特征。

缺失值的處理

在構造決策樹的時候,對於有缺失值的特征的處理,需要解決以下兩個問題:

  1. 如何在屬性值缺失的情況下進行划分屬性選擇?
  2. 給定划分屬性,若樣本在該屬性上的值缺失,如何對樣本進行划分?

問題1

  對於節點,樣本集合為\(D\),假設某特征\(A\)有部分缺失,用\(D_{缺失}\)代表特征\(A\)缺失的樣本集合,\(D_{未缺失}\)代表屬性\(A\)未缺失的集合。在計算屬性\(A\)的信息增益時,使用\(D_{未缺失}\)計算信息增益\(G_{未缺失}\),然后,乘以一個權重作為最終的信息增益。

\[G = \frac{|D_{未缺失}|}{|D|}G_{未缺失} \]

  可以看出,這個方法的思路是,使用未缺失的那部分樣本來計算信息增益。但是由於只使用了部分數據,在跟其他特征“競爭”最優特征的時候是不公平的。因此,對這個信息增益乘以一個權重,做一個縮小,以減弱缺失特征的“競爭力”,這個權重使用的是特征\(A\)的“完備率”,也就是說,缺失越嚴重,信息增益越低,這個特征也就越不可能被選為最優特征。

問題2

  上一個問題解決了選擇最優特征的時候缺失值的處理方法,如果\(A\)特征沒被選為最優特征,那缺失值對之后的數據划分並沒有影響。但是,當特征\(A\)被選為最優划分特征的時候,缺失值的樣本該划分到哪個分支中呢?

  這里,選擇將含有缺失值的樣本同時划分到所有分支中,但是,每個樣本並不是完整的樣本,而是樣本的“一部分”。在計算集合信息熵的時候,每個類別的概率\(p_k\)使用的其實是古典概型的頻率,比如,節點共有100個樣本,屬於類別1的樣本有70個,每個樣本的權重都是1,則\(p_1=0.7\)。而對於進入這個節點的“一部分樣本”卻不能占有1的權重,只能占有一部分權重。這個權重就是缺失樣本進入該節點的期望權重,也就是進入該節點的概率,也就是進入該節點的樣本點數量與總數量之比。

  舉個例子,某個節點共100個樣本,其中特征\(A\)缺失的有20個,根據特征\(A\)進行了划分,其中節點1有30個樣本,節點2有50個樣本。在對缺失的20個樣本划分的時候,每個樣本都進入了節點1和2,也就是說,節點1中最終有50個樣本,節點2中有70個樣本。但是在節點1中,含缺失值的樣本的權重不是1,而是0.375,同理,在節點2中,這些樣本的權重是0.625。

  同理,在進行預測時,如果輸入變量某特征缺失,則采用同樣的方法,最終樣本點可能被分配到不同的節點中,對節點按照節點概率\(P\)以及前面所說的權重加權即可。

剪枝的策略

  充分生長的決策樹由於復雜度很高,很容易對訓練集過擬合,導致泛化能力差。因此,需要控制決策樹復雜度,降低過擬合程度,提高泛化能力。其主要策略有兩種:預剪枝和后剪枝。

預剪枝

  預剪枝是指在決策樹生成的過程中進行的剪枝操作。在每次划分節點時,通過對預先留出的驗證集的預測精度的對比,決定是否要划分該節點。比如,在節點划分前,用決策樹對驗證集分類的准確度是80%,划分后只有70%,那么就會停止該節點的繼續生長。

  可以看出,預剪枝使得很多分支沒有展開,這不僅降低了過擬合的風險,同時,使得決策樹的訓練時間顯著降低。但是,由於預剪枝使用的是一種貪心算法,即雖然在本層划分不會提高精度,但是當節點二次划分的時候可能會提高精度,因此,可能會有欠擬合的風險。

后剪枝

  后剪枝是一種先構建完整的決策樹,再去除部分節點的方法。其方法是,構建一顆決策樹,假設決策樹深度為k,則第k層為葉節點,選擇第k-1層的節點,去除其第k層划分,如果驗證集精度上升,則刪除該划分,否則保留。

  不難看出,后剪枝比預剪枝更為優越,通常它會保留更多的節點並具有更強的泛化能力,但是,由於需要構建完整的樹並且驗證多個節點,模型的復雜程度會更高。

CART(Classification And Regression Tree)

分類樹

  CART分類樹與前面的算法相比主要有以下幾點不同:

  • 特征選擇:使用基尼系數作為特征選擇標准,避免了大量對數計算。
  • 特征划分:對於連續特征,使用的方法與C4.5相同,都是進行離散化二分,不同的是可以在后續繼續使用。對於離散特征,也進行二分划分。

回歸樹

  回歸樹與分類樹的基本結構十分相似,主要有以下兩個不同:

  1. 無論是基尼系數還是信息增益都是針對分類變量的評價方式,針對連續變量,要使用偏差平方和等。
  2. 預測時,分類樹是使用每個節點內,樣本數量最多的類別作為節點的預測類別,連續變量使用節點內樣本均值作為預測值。

預測值的選擇

  對於每一個節點,為什么要選擇均值作為預測值呢?

  假設節點\(t\)的預測值為\(C_t\),節點有\(n\)個樣本,則該節點的損失

\[L_t=\sum_{i=0}^n(y_i-C_t)^2 \]

則有

\[\begin{split} L_t & = \sum_{i=1}^{n}(y_i-\bar y+\bar y-C_t)^2\\&=\sum_{i=1}^n(y_i-\bar y)^2+\sum_{i=1}^n[2(y_i-\bar y)(\bar y-C_t)+(\bar y-C_t)^2]\\&=\sum_{i=1}^n(y_i-\bar y)^2 +\sum_{i=1}^n(2y_i-\bar y-C_t)(\bar y-C_t)\\&=\sum_{i=1}^n(y_i-\bar y)^2 +(\bar y-C_t)(2n\bar y-n\bar y-nC_t)\\&=\sum_{i=1}^n(y_i-\bar y)^2 +n(\bar y-C_t)^2\end{split} \]

  即對於所有的常數\(C_t\),使得損失\(L_t\)最小的常數就是均值。

特征選擇

  由於Cart是二叉樹,在回歸時,使用使得划分后的兩節點的均方誤差和最小的划分組合:

\[\underbrace{min}_{A,s}\Bigg[\underbrace{min}_{c_1}\sum\limits_{x_i \in D_1(A,s)}(y_i - c_1)^2 + \underbrace{min}_{c_2}\sum\limits_{x_i \in D_2(A,s)}(y_i - c_2)^2\Bigg] \]

  中括號內的最小化指的是針對某個具體的特征,在各種不同的切分節點(針對連續變量)或二分方式(針對離散變量)中,使得損失最小的那種,外層最小化指的是,針對不同的特征,選擇最優的划分特征。

python實現

  如下所示,這里實現了三個剪枝方法,分別是min_samples_split(某節點樣本數小於該值時停止划分)、min_leaf_sample(划分后子節點小於該值舍棄此次划分)、min_impurity_decrease(划分后,偏差平方和增量絕對值小於該值舍棄此次划分)。這里min_impurity_decrease=0.01很小,沒有起到作用,這時,可以看出,再最小划分數為4以及葉片最小樣本數為4的情況下,MSE為0.02426與sklearn結果相同。

import numpy as np
import pandas as pd
import json
from sklearn.tree import DecisionTreeRegressor


class RegressionTree:
    def __init__(self, X, y, min_samples_split=4, min_impurity_decrease=0.01, min_leaf_sample=4):
        self.features = np.array(X).reshape(X.shape[0], -1)
        self.labels = np.array(y).reshape(y.shape[0], -1)
        self.data = np.hstack((self.features, self.labels))
        self.min_samples_split = min_samples_split
        self.min_impurity_decrease = min_impurity_decrease
        self.min_leaf_sample = min_leaf_sample

    def calSE(self, array):
        '''

        :param array:
        :return: 輸入數組與均值的偏差平方和
        '''
        return np.sum((array-np.mean(array))**2)

    def bestFeatCat(self, data, index):
        '''
        這里在循環遍歷的時候其實是遍歷了兩倍的數量
        :param data: 輸入矩陣,形式為[X y],其中y為連續變量,是回歸的目標值
        :param index: 類別變量所在的列索引
        :return: 最小偏差平方和,最小偏差平方和對應的划分[list1,list2]
        '''
        best_split = []
        best_se = np.inf
        original_se = self.calSE(data)
        uniqueVlaue = np.unique(data[:, index])
        n = len(uniqueVlaue)
        for i in range(1, 2**n-1):
            chooseKey=[]
            for j in range(n):
                if (i >> j) % 2 == 1:
                    chooseKey.append(j)
            filter = pd.Series(data[:, index]).isin(uniqueVlaue[chooseKey])
            left_node = data[filter]
            right_node = data[~filter]
            if len(left_node) < self.min_leaf_sample:
                continue
            if len(right_node) < self.min_leaf_sample:
                continue
            SE = self.calSE(left_node[:, -1]) + self.calSE((right_node[:, -1]))
            if SE < best_se:
                best_se = SE
                best_split = [uniqueVlaue[chooseKey], np.setdiff1d(uniqueVlaue, uniqueVlaue[chooseKey])]
        if original_se - best_se < self.min_impurity_decrease:
            return None, np.mean(data[:, -1])
        return best_se, best_split

    def bestFeatCon(self, data, index):
        '''
        大於閾值划分為左節點
        :param data: 輸入矩陣,形式為[X y],其中y為連續變量,是回歸的目標值
        :param index: 連續變量特征所在列索引
        :return: 最小偏差平方和,最小偏差平方和對應的閾值
        '''
        sorted = data[:, index].copy()
        sorted.sort()
        threshold = []
        best_se = np.inf
        original_se = self.calSE(data)
        best_split = None
        for i in range(sorted.shape[0]-1):
            avg = sorted[[i, i+1]].mean()
            threshold.append(avg)
        for j in threshold:
            series = pd.Series(data[:, index])
            filter = series > j
            left_node = data[filter]
            right_node = data[~filter]
            if len(left_node) < self.min_samples_split:
                continue
            if len(right_node) < self.min_samples_split:
                continue
            SE = self.calSE(left_node[:, -1]) + self.calSE((right_node[:, -1]))
            if SE < best_se:
                best_se = SE
                best_split = j
        if best_se == np.inf:
            return None, np.mean(data[:, -1])
        if original_se - best_se < self.min_impurity_decrease:
            return None, np.mean(data[:, -1])
        return best_se, best_split

    def chooseBestFeat(self, data):
        '''
        用於選擇最優划分特征
        :param data:輸入矩陣,形式為[X y],其中y為連續變量,是回歸的目標值
        :return: 最優特征所在列的索引,最小的SE,最佳特征對應的划分(分類特征為)
        '''
        if len(data) <= self.min_samples_split:
            return None, None, data[:, -1].mean(), None
        best_feat = -1
        best_se = np.inf
        best_split = -1
        for i in range(data.shape[1]-1):
            if all(data[:, i].astype(int) == data[:, i]):
                se = self.bestFeatCat(data, i)[0]
                split = self.bestFeatCat(data, i)[1]
                type = 'categorical'
            else:
                se = self.bestFeatCon(data, i)[0]
                split = self.bestFeatCon(data, i)[1]
                type = 'continuous'
            if se is None:
                continue
            if (se < best_se) or (se is None):
                best_feat = i
                best_se = se
                best_split = split
        if best_se == np.inf:
            best_feat = None
            best_split = np.mean(data[:, -1])
        return best_feat, best_se, best_split, type

    def splitData(self, data, feature, values, FeatType):

        if FeatType == 'categorical':
            left = data[pd.Series(data[:, feature]).isin(values[0])]
            right = data[pd.Series(data[:, feature]).isin(values[1])]
        if FeatType == 'continuous':
            left = data[pd.Series(data[:, feature]) > values]
            right = data[pd.Series(data[:, feature]) <= values]
        return left, right

    def createTree(self, data):
        '''

        :param data:
        :return:
        '''

        if len(data) <= self.min_leaf_sample:
            return np.mean(data[:, -1])
        feature, se, values, FeatType = self.chooseBestFeat(data)
        if feature is None:
            return values
        tree = dict()
        tree['splitInd'] = feature
        tree['splitValue'] = values
        tree['FeatType'] = FeatType
        left, right = self.splitData(data, feature, values, FeatType)
        tree['left'] = self.createTree(left)
        tree['right'] = self.createTree(right)
        return tree

    def fit(self):
        self.tree = self.createTree(self.data)

    def predict_vec(self, x, tree=None):
        x = np.array(x)
        if tree is None:
            tree = self.tree
        if x[tree.get('splitInd')] > tree.get('splitValue'):
            if isinstance(tree.get('left'), dict):
                return self.predict_vec(x, tree.get('left'))
            else:
                return tree.get('left')
        else:
            if isinstance(tree.get('right'), dict):
                return self.predict_vec(x, tree.get('right'))
            else:
                return tree.get('right')

    def predict(self, data):
        l = []
        for i in data:
            l.append(self.predict_vec(np.array(i)))
        return np.array(l)

    def calMSE(self, y_true, y_pre):
        y_true = np.array(y_true)
        y_pre = np.array(y_pre)
        return np.mean((y_true-y_pre)**2)


if __name__ == '__main__':
    data = pd.read_csv('./ex00.txt', sep='\t', header=None)
    tree = RegressionTree(data[0], data[1])
    a = tree.splitData(data.values, 0, 0.498035, 'continuous')[0]
    b = tree.splitData(data.values, 0, 0.498035, 'continuous')[1]
    tree.fit()
    y_pre = tree.predict(data.iloc[:, 0].values.reshape(-1, 1))
    print(f"MSE:{tree.calMSE(y_pre, data.iloc[:, -1])}")
    dt = DecisionTreeRegressor(min_samples_split=4,
                               min_samples_leaf=4).fit(data.iloc[:, 0].values.reshape(-1, 1), data.iloc[:,1])
    y_pre_dt = dt.predict(data.iloc[:, 0].values.reshape(-1, 1))
    print(f"SKlearnMSE:{tree.calMSE(y_pre_dt, data.iloc[:, -1])}")
MSE:0.024262198879467293
SKlearnMSE:0.024262198879467293

多變量決策樹

  假設現在輸入數據只有兩個連續特征,且特征可重復使用。由於在划分的時候,划分條件是諸如\(FeatureA>40\),因此,如下圖所示,使用決策樹進行決策的過程,可以看作將二維平面不斷使用垂直於兩坐標軸的直線進行分割。

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
import matplotlib.pyplot as plt
import mglearn

X,y = make_moons(n_samples=100,noise=0.25,random_state=3)
X_train,X_test,y_train,y_test = train_test_split(X,y,stratify=y,random_state=42)

DSC = DecisionTreeClassifier().fit(X_train,y_train)
mglearn.plots.plot_tree_partition(X_train,y_train,DSC)
plt.show()


image

  如下圖所示,多變量決策樹改變了\(FeatureA>40\)的划分方式,使用諸如

\[-0.3\times FeatureA+0.7\times FeatureB>20 \]

的方法進行划分,使得決策邊界可以是傾斜的直線或曲線。

image

Sklearn決策樹

sklearn構造決策樹

# 使用乳腺癌數據集構造決策樹
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()
X_train,X_test,y_train,y_test = train_test_split(cancer.data,cancer.target,stratify=cancer.target,random_state=42)

##使用乳腺癌數據集構造決策樹
DTC =DecisionTreeClassifier(max_depth=3).fit(X_train,y_train)

print('訓練集精度:{}'.format(DTC.score(X_train,y_train)))
print('測試集精度:{}'.format(DTC.score(X_test,y_test)))
訓練集精度:0.9741784037558685
測試集精度:0.9300699300699301

  由於樹深度很大,訓練精度很高,但是測試精度較低,下面限制樹深查看結果:

##使用乳腺癌數據集構造決策樹
DTC =DecisionTreeClassifier(max_depth=3).fit(X_train,y_train)

print('訓練集精度:{}'.format(DTC.score(X_train,y_train)))
print('測試集精度:{}'.format(DTC.score(X_test,y_test)))
訓練集精度:0.9765258215962441
測試集精度:0.9440559440559441

分析決策樹

from sklearn.tree import export_graphviz
export_graphviz(DTC,out_file='tree.dot',class_names=["malignant","benign"],feature_names=cancer.feature_names)
import graphviz
with open('tree.dot','r') as f:
    dot_graph = f.read()
graphviz.Source(dot_graph)


image

  第一層,在根節點處,根據特征worst perimeter是否小於等於112.8將數據划分為兩類,划分前基尼系數0.468,節點共426個樣本,其中正負樣本分別為159和267個,節點類別為benign。第二層為划分后數據,划分后基尼系數分別為0.161和0.106。按照樣本量占比對其進行加權即可得出本層的總基尼系數,\(0.161\times(284\div426)+0.106\times(142\div426)=0.143\) 。可以看出基尼系數顯著下降。

樹的特征重要性

  樹的特征重要性可以使用tree.feature_importance_查看,特征重要性基於決策樹的實現算法使用gini或者香農熵進行計算,特征重要性的和為1,且每個特征的重要性位於 [0,1] 之間。

  如上圖中,每次使用一個特征划分后,就會產生一個基尼系數的增量,以這個增量大小衡量特征的重要性,即求得所有特征對應的划分增量占總增量的比值,即為特征重要性。

  具體的,還是以上圖,以worst radius為例,之前求出在根節點處,使用該特征划划分后的基尼系數

\[0.161×(284÷426)+0.106×(142÷426)=0.14266666 \]

然后計算完全划分后的所有葉節點的基尼系數(有三個葉節點是純凈的):

\[251/426*0.024+0.375*12/426+20/426*0.18+0.48*5/426=0.03878873239436619 \]

初始基尼系數為\(0.468\),worst radius的基尼系數增量就是:

\[0.468-0.14266666=0.32533334 \]

總的基尼系數增量為

\[0.468-0.03878873239436619=0.42921126760563383 \]

因此worst radius的特征重要性就是

\[0.32533334/0.42921126760563383=0.7579794953074752 \]

DTC.feature_importances_
array([0.        , 0.        , 0.        , 0.        , 0.        ,
       0.        , 0.        , 0.        , 0.        , 0.        ,
       0.        , 0.0504697 , 0.        , 0.01063382, 0.        ,
       0.        , 0.        , 0.        , 0.        , 0.        ,
       0.75793681, 0.03465357, 0.        , 0.        , 0.        ,
       0.        , 0.01896644, 0.12733965, 0.        , 0.        ])

  可以看出,與sklearn給出的結果基本相同,下面將特征重要性可視化:

def plot_feature_importance_cancer(model):
    plt.subplots(figsize=(10,8))
    plt.barh(range(cancer.data.shape[1]),model.feature_importances_)
    plt.yticks(range(cancer.data.shape[1]),cancer.feature_names)
    plt.xlabel('Feature Importance')
    plt.ylabel('Feature Name')
plot_feature_importance_cancer(DTC)


image

預測概率值

  對於分類樹,有一個方法是predict_prob可以給出每個樣本被分為每個類別的概率,這個概率是使用每個葉節點中不同類別樣本占比所得出的條件概率。如下所示,共有7個葉節點,因此共有七種不同的概率,其中最多的節點有251個樣本,且被分類為benign。

  在實際使用時,DTC.predict()方法使用默認的0.5作為閾值,我們可以根據實際情況,選用不同的閾值,從而調整預測的precision和recall。

pd.DataFrame(DTC.predict_proba(X_train)).value_counts()
0         1       
0.015936  0.984064    251
0.992248  0.007752    129
0.941176  0.058824     17
0.444444  0.555556      9
0.142857  0.857143      7
0.857143  0.142857      7
0.000000  1.000000      6
dtype: int64

skearn參數說明

參數 DecisionTreeClassifier DecisionTreeRegressor

criterion:特征選擇標准

{“gini”, “entropy”}, default=”gini”,前者代表基尼系數,后者代表信息增益。一般說使用默認的基尼系數"gini"就可以,即CART算法。 

 {“squared_error”, “friedman_mse”, “absolute_error”, “poisson”}, default=”squared_error”。分別是“均方誤差MSE”“friedmanMSE”“平均絕對誤差MAE”“泊松偏差”,一般使用默認的"mse"。

splitter:特征划分點選擇標准

可以使用"best"或者"random"。前者在特征的所有划分點中找出最優的划分點。后者是隨機的在部分划分點中找局部最優的划分點。

比如在Cart離散變量特征最優選擇的時候,因為要把所有的特征組合成兩組,大概有$2^{n-1}$的數量級的循環遍歷,計算量巨大。因此,默認的"best"適合樣本量不大的時候,而如果樣本數據量非常大,此時決策樹構建推薦"random" 

max_features:划分時考慮的最大特征數

int, float or {“auto”, “sqrt”, “log2”}, default=None。可以使用很多種類型的值,默認是"None",和"auto"一樣,意味着划分時考慮所有的特征數(分類樹"auto"與"sqrt"一樣);如果是"log2"意味着划分時最多考慮$log_2N$個特征;如果是"sqrt"或者意味着划分時最多考慮$\sqrt{N}$個特征。如果是整數,代表考慮的特征絕對數。如果是浮點數,代表考慮特征百分比,即考慮int(max_features * n_features)取整后的特征數。其中N為樣本總特征數。

一般來說,如果樣本特征數不多,比如小於50,我們用默認的"None"就可以了,如果特征數非常多,我們可以靈活使用剛才描述的其他取值來控制划分時考慮的最大特征數,以控制決策樹的生成時間。

max_depth:決策樹最大深度

 int, default=None。決策樹的最大深度,默認可以不輸入,如果不輸入的話,決策樹在建立子樹的時候不會限制子樹的深度。一般來說,數據少或者特征少的時候可以不管這個值。如果模型樣本量多,特征也多的情況下,推薦限制這個最大深度,具體的取值取決於數據的分布。常用的可以取值10-100之間。

min_samples_split:內部節點再划分所需最小樣本數

int or float, default=2。這個值限制了子樹繼續划分的條件,如果某節點的樣本數少於min_samples_split,則不會繼續再嘗試選擇最優特征來進行划分。 默認是2.如果樣本量不大,不需要管這個值。如果樣本量數量級非常大,則推薦增大這個值。如果輸入為小於1的float,即百分比,則最小節點划分樣本數為ceil(min_samples_leaf * n_samples)

min_samples_leaf:葉子節點最少樣本數

 int or float, default=1。這個值限制了葉子節點最少的樣本數,如果某葉子節點數目小於樣本數,則會和兄弟節點一起被剪枝。 默認是1,可以輸入最少的樣本數的整數,或者最少樣本數占樣本總數的百分比,同樣的,百分比表示的最小樣本數為ceil(min_samples_leaf * n_samples)

min_weight_fraction_leaf:葉子節點最小的樣本權重和

float, default=0.0。這個值限制了葉子節點所有樣本權重和的最小值,如果小於這個值,則會和兄弟節點一起被剪枝。 默認是0,就是不考慮權重問題。一般來說,如果我們有較多樣本有缺失值,或者分類樹樣本的分布類別偏差很大,就會引入樣本權重,這時我們就要注意這個值了。

max_leaf_nodes:最大葉子節點數

 int, default=None。通過限制最大葉子節點數,可以防止過擬合,默認是"None”,即不限制最大的葉子節點數。如果加了限制,算法會建立在最大葉子節點數內最優的決策樹。如果特征不多,可以不考慮這個值,但是如果特征分成多的話,可以加以限制,具體的值可以通過交叉驗證得到。

class_weight:類別權重

dict, list of dict or “balanced”, default=None。指定樣本各類別的的權重,主要是為了防止訓練集某些類別的樣本過多,導致訓練的決策樹過於偏向這些類別。這里可以自己指定各個樣本的權重,或者用“balanced”,如果使用“balanced”,則算法會自己計算權重,樣本量少的類別所對應的樣本權重會高。當然,如果你的樣本類別分布沒有明顯的偏倚,則可以不管這個參數,選擇默認的"None"  不適用於回歸樹

min_impurity_split:節點划分最小不純度

 float, default=0.0。這個值限制了決策樹的增長,如果某節點的不純度加權增量(基尼系數,信息增益,均方差,絕對差)小於這個閾值,則該節點不再生成子節點。即為葉子節點 。


免責聲明!

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



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