決策樹3: 特徵選擇之尋找最優劃分

  • 2019 年 12 月 23 日
  • 筆記

0x00 前言

決策樹演算法的三個步驟:特徵選擇、決策樹生成、決策樹剪枝。其中特徵選擇要解決的核心問題就是:

  • 每個節點在哪個維度上做劃分?
  • 某個維度在哪個值上做劃分?

劃分的依據是: 要讓數據劃分成兩部分之後,系統整體的資訊熵降低。

具體方法是: 對所有的劃分可行性進行搜索。下一篇我們模擬在一個節點上進行搜索,找到一個節點上資訊熵的最優劃分。

那麼問題來了: 我們如何找到各個特徵/節點上的最優劃分呢?

0x01 資訊熵的最優劃分

1.1 模擬貸款申請

現在我們以銀行貸款申請業務為例,模擬四個特徵,分別是:年齡、有工作、有房子、信貸情況。

下面就通過程式碼實現,找到當前應該在哪個特徵維度上的哪個值進行最優劃分:

1.2 程式碼實現

import numpy as npfrom collections import Counterfrom math import log  # 每列:['年齡','有工作','有自己的房子','信貸情況','是否申請貸款']dataSet=np.array([[0, 0, 0, 0, 0],                  [0, 0, 0, 1, 0],                  [0, 1, 0, 1, 1],                  [0, 1, 1, 0, 1],                  [0, 0, 0, 0, 0],                  [1, 0, 0, 0, 0],                  [1, 0, 0, 1, 0],                  [1, 1, 1, 1, 1],                  [1, 0, 1, 2, 1],                  [1, 0, 1, 2, 1],                  [2, 0, 1, 2, 1],                  [2, 0, 1, 1, 1],                  [2, 1, 0, 1, 1],                  [2, 1, 0, 2, 1],                  [2, 0, 0, 0, 0]])featList = ['年齡','有工作','有自己的房子','信貸情況']  """函數說明:計算給定標籤的經驗熵(資訊熵)Parameters:    y:使用標籤y計算資訊熵,,此時傳遞y是多維數組    計算資訊熵需要每種類別出現的概率p,因此傳入包含分類資訊的標籤yReturns:    entropy:經驗熵"""def calEntropy(y):    # 計數器,統計y中所有類別出現的次數    # 扁平化,將嵌套的多維數組變成一維數組    counter = Counter(y.flatten())    entropy = 0    for num in counter.values():        p = num / len(y)        entropy += -p * log(p)    return entropy  """函數說明:根據傳遞進來的特徵維度及值,將數據劃分為2類Parameters:    X,y,featVec,value:特徵向量、標籤、特徵維度、值Returns:    返回劃分為兩類的後的數據"""def split(X, y, featVec, value):    # 使用維度featVect上的value,將數據劃分成左右兩部分    # 得到的布爾向量,傳入array中做索引,即可找出滿足條件的相應數據(布爾屏蔽)    index_a = (X[:,featVec] <= value)    index_b = (X[:,featVec] > value)    return X[index_a], X[index_b], y[index_a], y[index_b]  """函數說明:尋找最優劃分Parameters:    X,y:特徵向量、標籤Returns:    返回最優熵,以及在哪個維度、哪個值進行劃分"""def try_split(X, y):    # 搞一個熵的初始值:正無窮    best_entropy = float('inf')    best_featVec = -1    # 特徵向量    best_value = -1    # 遍歷每一個特徵維度(列)    for featVec in range(X.shape[1]):        # 然後需要找到每個特徵維度上的劃分點。        # 找出該維度上的每個兩個樣本點的中間值,作為候選劃分點。        # 為了方便尋找候選劃分點,可以對該維度上的數值進行排序,        # argsort函數返回的是數組值從小到大的索引值(不打亂原來的順序)        sort_index = np.argsort(X[:,featVec])                for i in range(1, len(X)):            if X[sort_index[i-1], featVec] != X[sort_index[i], featVec]:                value = (X[sort_index[i-1], featVec] + X[sort_index[i], featVec]) / 2                X_l, X_r, y_l, y_r = split(X, y, featVec, value)                # 要求最優劃分,需要看在此劃分下得到的兩個分類數據集的熵之和是否是最小的                entropy = calEntropy(y_l) + calEntropy(y_r)                if entropy < best_entropy:                    best_entropy, best_featVec, best_value = entropy, featVec, value    return best_entropy, best_featVec, best_value          best_entropy, best_featVec, best_value = try_split(X, y)print("最優熵:", best_featVec)print("在哪個維度熵進行劃分:", best_featVec)print("在哪個值上進行劃分:", best_value)

輸出:最優熵:0.6365141682948128在哪個維度熵進行劃分:2在哪個值上進行劃分:0.5

也就是說,根據窮舉各個欄位上的最優熵,可以得知,在第3個特徵(有自己的房子)上,以0.5為閾值進行分類,可以得到最小熵。

0x02 資訊增益&資訊增益率最優劃分

2.1 資訊增益最優劃分實現

PS:下面的程式碼是都是乾貨,多讀讀注釋,看懂了就理解透徹了

import numpy as npfrom collections import Counterfrom math import log  # 每列:['年齡','有工作','有自己的房子','信貸情況','是否申請貸款'],其中'是否申請貸款'是labeldataSet=np.array([[0, 0, 0, 0, 0],                  [0, 0, 0, 1, 0],                  [0, 1, 0, 1, 1],                  [0, 1, 1, 0, 1],                  [0, 0, 0, 0, 0],                  [1, 0, 0, 0, 0],                  [1, 0, 0, 1, 0],                  [1, 1, 1, 1, 1],                  [1, 0, 1, 2, 1],                  [1, 0, 1, 2, 1],                  [2, 0, 1, 2, 1],                  [2, 0, 1, 1, 1],                  [2, 1, 0, 1, 1],                  [2, 1, 0, 2, 1],                  [2, 0, 0, 0, 0]])X = dataSet[:,:4]y = dataSet[:,-1:]strs = ['年齡','有工作','有自己的房子','信貸情況','是否申請貸款']    """函數說明:計算經驗熵Parameters:    dataSet:樣本數據集DReturns:    entory:經驗熵"""def calEntropy(dataSet):    #返回數據集行數    numEntries=len(dataSet)    #保存每個標籤(label)出現次數的字典:<label:出現次數>    labelCounts={}    #對每組特徵向量進行統計    for featVec in dataSet:        #提取標籤資訊        currentLabel=featVec[-1]        #如果標籤沒有放入統計次數的字典,添加進去        if currentLabel not in labelCounts.keys():            labelCounts[currentLabel]=0        #label計數        labelCounts[currentLabel]+=1        entory=0.0    #計算經驗熵    for key in labelCounts:        #選擇該標籤的概率        prob=float(labelCounts[key])/numEntries         #利用公式計算        entory-=prob*log(prob,2)    return entory    """函數說明:得到當前特徵條件下的小類的所有樣本集合(即不包含當前特徵的特徵樣本集)Parameters:    dataSet:樣本數據集D    curtFeatIndex:當前用來劃分數據集的特徵A的位置    categories:特徵A所有可能分類的集合Returns:    otherFeatSets:不包含當前特徵的特徵樣本集"""def currentConditionSet(dataSet, curtFeatIndex, categroy):    otherFeatSets = []    # 對於數據集中的所有特徵向量,拋去當前特徵後拼接好的集合    for featVec in dataSet:        if featVec[curtFeatIndex] == categroy:            otherFeatSet = np.append(featVec[:curtFeatIndex],featVec[curtFeatIndex+1:])            otherFeatSets.append(otherFeatSet)     return otherFeatSets    """函數說明:在選擇當前特徵的條件下,計算熵,即條件熵Parameters:    dataSet:樣本數據集D    curtFeatIndex:當前用來劃分數據集的特徵A的位置    categories:特徵A所有可能分類的集合Returns:    conditionalEnt:返回條件熵"""def calConditionalEnt(dataSet, curtFeatIndex, categories):    conditionalEnt = 0    # 對於每一個分類,計算選擇當前特徵的條件下條件熵    # 比如在選擇「年齡」這一特徵下,共有「老中青」三個小分類    for categroy in categories:        # 得到當前特徵條件下的小類的所有樣本集合,即不包含當前特徵的特徵樣本集        # 如得到在選擇「青年」這個小類下一共有5個樣本,且不包含「年齡」這一特徵        cdtSetCategroy = currentConditionSet(dataSet, curtFeatIndex, categroy)        # 計算當前特徵條件下的小分類,佔總分類的比例        prob = len(cdtSetCategroy) / float(dataSet.shape[0])        # 累加得到條件熵        conditionalEnt += prob * calEntropy(cdtSetCategroy)    return conditionalEnt    """函數說明:計算資訊增益Parameters:    baseEntropy:劃分樣本集合D的熵是為H(D),即基本熵    dataSet:樣本數據集D    curtFeatIndex:當前用來劃分數據集的特徵A的位置Returns:    infoGain:資訊增益值"""def calInfoGain(baseEntropy,dataSet,curtFeatIndex):        conditionalEnt = 0.0        # categories是所有特徵向量中當前特徵的對應值的set集合(去重複)    # 相當於該特徵一共有幾種分類,如「年齡」這一特徵,分為「老中青」三類    categories = set(dataSet[:,curtFeatIndex])        # 計算劃分後的數據子集(給定特徵A的情況下,數據集D)的條件熵(經驗條件熵)H(D|A)    conditionalEnt = calConditionalEnt(dataSet,curtFeatIndex,categories)        # 計算資訊增益:g(D,A)=H(D)−H(D|A)    infoGain = baseEntropy - conditionalEnt        #列印每個特徵的資訊增益    print("第%d個特徵的增益為%.3f" % (curtFeatIndex, infoGain))    return infoGain    """函數說明:尋找最優劃分Parameters:    dataSet:數據集Returns:    列印最優劃分結果"""def optimalPartition(dataSet):    bestInfoGain = -1   # 最佳資訊增益初始值    bestFeatVec = -1    # 最佳劃分的特徵向量    # 劃分前樣本集合D的熵H(D),即基本熵    baseEntropy = calEntropy(dataSet)        # 遍歷每一個特徵維度(列),得到基於當前特徵劃分的資訊增益    for curtFeatIndex in range(dataSet.shape[1]-1):                # 計算資訊增益        infoGain = calInfoGain(baseEntropy, dataSet, curtFeatIndex)                # 選取最優資訊增益的劃分        if (infoGain > bestInfoGain):            #更新資訊增益,找到最大的資訊增益            bestInfoGain = infoGain            #記錄資訊增益最大的特徵的索引值            bestFeatVec = curtFeatIndex        print("最佳的劃分為第%d個特徵,是」%s「,資訊增益為%.3f" % (bestFeatVec,featList[bestFeatVec],bestInfoGain))    return bestFeatVec  optimalPartition(dataSet)

輸出:

第0個特徵的增益為0.083第1個特徵的增益為0.324第2個特徵的增益為0.420第3個特徵的增益為0.363最佳的劃分為第2個特徵,是」有自己的房子「,資訊增益為0.420

2.2 資訊增益率最優劃分實現

根據資訊增益率的定義,對上面的程式碼進行改造,可以得到資訊增益率的最優選擇實現

"""函數說明:計算懲罰參數,資訊增益g(D,A)與訓練數據集D關於特徵A的值的熵HA(D)之比Parameters:    dataSet:樣本數據集D    curtFeatIndex:當前用來劃分數據集的特徵A的位置    categories:特徵A所有可能分類的集合Returns:    conditionalEnt:懲罰參數"""def calPenaltyPara(dataSet, curtFeatIndex, categories):    penaltyItem = 1    # 對於每一個分類,計算選擇當前特徵的條件下條件熵    # 比如在選擇「年齡」這一特徵下,共有「老中青」三個小分類    for categroy in categories:        # 得到當前特徵條件下的小類的所有樣本集合,即不包含當前特徵的特徵樣本集        # 如得到在選擇「青年」這個小類下一共有5個樣本,且不包含「年齡」這一特徵        cdtSetCategroy = currentConditionSet(dataSet, curtFeatIndex, categroy)        # 計算當前特徵條件下的小分類,佔總分類的比例        prob = len(cdtSetCategroy) / float(dataSet.shape[0])        # 累加得到懲罰項        penaltyItem += -prob * log(prob,2)    return penaltyItem  """函數說明:計算資訊增益率(懲罰參數 * 資訊增益)Parameters:    baseEntropy:劃分樣本集合D的熵是為H(D),即基本熵    dataSet:樣本數據集D    curtFeatIndex:當前用來劃分數據集的特徵A的位置Returns:    infoGain:資訊增益值"""def calInfoGainRate(baseEntropy,dataSet,curtFeatIndex):    infoGainRate = 0.0    # 計算資訊增益    infoGain = calInfoGain(baseEntropy,dataSet,curtFeatIndex)    # 得到該特徵的所有分類    categories = set(dataSet[:,curtFeatIndex])    # 計算懲罰項    penaltyItem = calPenaltyPara(dataSet, curtFeatIndex, categories)    # 計算資訊增益率    infoGainRatio = infoGain / penaltyItem        #列印每個特徵的資訊增益率    print("第%d個特徵的增益率為%.3f" % (curtFeatIndex, infoGainRatio))    return infoGainRatio  """函數說明:尋找最優劃分Parameters:    dataSet:數據集Returns:    列印最優劃分結果"""def optimalPartition(dataSet):    bestInfoGainRatio = 0.0   # 最佳資訊增益率初始值    bestFeatVec = -1    # 最佳劃分的特徵向量    # 劃分前樣本集合D的熵H(D),即基本熵    baseEntropy = calEntropy(dataSet)        # 遍歷每一個特徵維度(列),得到基於當前特徵劃分的資訊增益    for curtFeatIndex in range(dataSet.shape[1]-1):                # categories是所有特徵向量中當前特徵的對應值的set集合(去重複)        # 相當於該特徵一共有幾種分類,如「年齡」這一特徵,分為「老中青」三類        #categories = set(dataSet[:,curtFeatIndex])                # 計算資訊增益率        infoGainRatio = calInfoGainRate(baseEntropy, dataSet, curtFeatIndex)                # 選取最優資訊增益率的劃分        if (infoGainRatio > bestInfoGainRatio):            #更新資訊增益率,找到最大的資訊增益率            bestInfoGainRatio = infoGainRatio            #記錄資訊增益率最大的特徵的索引值            bestFeatVec = curtFeatIndex        print("最佳的劃分為第%d個特徵,是」%s「,資訊增益率為%.3f" % (bestFeatVec,strs[bestFeatVec],bestInfoGainRatio))    return  optimalPartition(dataSet)

輸出:

第0個特徵的增益率為0.032第1個特徵的增益率為0.169第2個特徵的增益率為0.213第3個特徵的增益率為0.141最佳的劃分為第2個特徵,是」有自己的房子「,資訊增益率為0.213

0x03 基尼係數最優劃分

3.1 基尼係數最優劃分實現

"""函數說明:計算基尼係數Parameters:    y:使用標籤y計算資訊熵,此時傳遞y是多維數組Returns:    entropy:經驗熵"""def calGini(y):    # 計數器,統計y中所有類別出現的次數    # 扁平化,將嵌套的多維數組變成一維數組    counter = Counter(y.flatten())    gini = 1    for num in counter.values():        p = num / len(y)        gini -= p ** 2    return gini    """函數說明:尋找最優劃分Parameters:    X,y:特徵向量、標籤Returns:    返回最優熵,以及在哪個維度、哪個值進行劃分"""def try_split(X, y):    # 搞一個基尼係數的初始值:正無窮    bestGini = float('inf')    bestFeatVec = -1    # 特徵向量    bestValue = -1    # 遍歷每一個特徵維度(列)    for featVec in range(X.shape[1]):        # 然後需要找到每個特徵維度上的劃分點。        # 找出該維度上的每個兩個樣本點的中間值,作為候選劃分點。        # 為了方便尋找候選劃分點,可以對該維度上的數值進行排序,        # argsort函數返回的是數組值從小到大的索引值(不打亂原來的順序)        sort_index = np.argsort(X[:,featVec])                for i in range(1, len(X)):            if X[sort_index[i-1], featVec] != X[sort_index[i], featVec]:                value = (X[sort_index[i-1], featVec] + X[sort_index[i], featVec]) / 2                X_l, X_r, y_l, y_r = split(X, y, featVec, value)                # 要求最優劃分,需要看在此劃分下得到的兩個分類數據集的熵之和是否是最小的                gini = calGini(y_l) + calGini(y_r)                if gini < bestGini:                    bestGini, bestFeatVec, bestValue = gini, featVec, value    return bestGini, bestFeatVec, bestValue  bestGini, bestFeatVec, bestValue = try_split(X, y)print("最優基尼係數:", bestGini)print("在哪個維度上進行劃分:", bestFeatVec)print("在哪個值上進行劃分:", bestValue)

輸出:

最優基尼係數:0.4444444444444445在哪個維度上進行劃分:2在哪個值上進行劃分:0.5

0xFF 總結

結合概念,好好看程式碼。程式碼研究明白,你就悟了!加油吧少年!