在上一章我們提到可以用遺傳演算法進行特徵選擇,這章就來講一講。
在構建分類模型時,經常需要對自變數進行篩選。特別是在異常檢測領域,特徵選擇是非常重要的,它會讓模型輸出業務上感興趣的異常點,而不是一些無關異常。通常情況下,離羣點往往會體現在幾個具體的特徵維度上,有效的特徵選擇既能降低模型訓練時間,又能保證模型精度。
本文主要介紹三種不同的特徵選擇方法,如下所示。
(1) 評分卡方法,該方法是風控行業常用的模型,利用信息價值(Information Value,以下簡稱IV)和證據權重(Weight of Evidence,以下簡稱WOE)選擇合適的特徵;
(2) 遺傳演算法,利用優化演算法來選擇特徵,選擇標記為1,不選標記為0,通過選擇最優個體、組合交叉和變異,使適應度函數達到最優解,進而得到合適的特徵;
(3) 峯度檢驗,通過對數據自身的分佈和統計量來估計該特徵是否包含異常值。
以上提到的三種方法,前兩種方法要依賴於部分標籤樣本,適合半監督或者監督學習,而第三種方法則完全不需要,適合無監督學習。
IV可以用來衡量自變數的預測能力。類似的指標還有信息增益和基尼係數,IV的計算是以WOE為基礎的。首先來介紹WOE。
WOE是對原始變數的一種編碼形式,要對一個變數進行編碼,首先要對它進行離散化(分箱處理)。分組後,對於第 i 組,WOE的計算公式如下所示。
表示該組中響應客戶(風險模型中,對應的是違約客戶,即正樣本的個體)佔所有樣本中響應客戶的比例, 是這個組中未響應客戶佔樣本中所有未響應客戶的比例,# 是這個組中響應客戶的數量,# 是這個組中未響應客戶的數量,# 是樣本中所有響應客戶的數量,# 是樣本中所有響應客戶的數量。
IV的計算公式如下所示。
有了一個變數各分組的IV值,就可以計算整個變數的IV值,計算公式如下所示。
下面講一個實例。
假設我們需要構建一個預測模型,預測向銀行貸款的客戶是否會違約,或者說預測的是貸款客戶的違約可能性有多大。假設已經有100000個客戶參加了測試,把這些客戶的響應結果作為數據集,可選變數如下所示。
(1) 最近三個月是否有貸款記錄;
(2) 最近一次的貸款金額;
(3) 最近一年是否違約過;
統計結果如下所示(已經離散化)。
可以看出,當前分組響應的比例越大,WOE值越大,如果該組響應比例大於整體樣本響應比例,WOE值為正,否則,WOE為負,兩者相等時,WOE為0.
接下來計算分組的IV值,如下所示。
可以看出,該分組響應比例與整體相差越大,IV值越大,否則越小。IV的取值範圍是0到正無窮。
再計算其它變數的IV值,得到如下結果。
(1) 最近三個月是否有貸款記錄:0.25022;
(2) 最近一次的貸款金額:0.49270;
(3) 最近一年是否違約過:1.56550;
由以上結果可以看出「最近一年是否違約過」是預測能力最高的變數。
首先由自然界物競天擇適者生存得到啟迪,衍生出遺傳演算法。
如果有一個國王,想讓國家裡的人都是好人,那麼他會怎麼做呢?
(1) 選出所有好人,然後讓他們交配產生自己的後代;
(2) 重複幾代(This repeats for a few generations.);
(3) 所有人都是好人。
再重新梳理一下上面的步驟。
(1) 定義國家的所有人民;
(2) 定義一個函數來判斷一個人是好人還是壞人;
(3) 選出好人交配產生後代(Then we selected good people for mating to produce their off-springs.)
(4) 最後,好人的後代取代壞人並不斷重複這個過程。
遺傳演算法的步驟可以用下圖來表示。
讓我們用遺傳演算法來解決一個實際的例子,比如揹包問題。
如果你將要去野外待一段時間,你只有一個最大承重只有30kg的揹包,現在有各種各樣的物品供你挑選,並且每種商品都有「生存點數」,「生存點數」越大,說明該物品維持生存的時間越長。所以我們的目標就是最大化生存點數。物品清單如下所示。
我們先來定義人羣(population),人羣裏的個體(individual)都有自己的chromosome(染色體),這裡的chromosome是一個binary strings,0代表不帶該物品,1代表帶上該物品。
怎樣判斷哪個好呢?生存點數大的就是好的,這裡的適應度函數就是生存點數。
對於A1來說,生存點數如下所示。
對於A2來說,生存點數則不如A1。
A1比A2更優。
接下來就是選擇(selection)。遺傳演算法常用的有好幾種選擇演算法。最經典的是輪盤賭選擇(Roulette Wheel Selection method)。把一個圓盤分成m份,其中m是population的數量,然後每一份的佔比等於該individuals的適應度與總適應度的比值。如下圖所示。
構造的圓盤如下所示。
還有一種方法是錦標賽選擇(Tournament Selection)。
錦標賽選擇演算法是遺傳演算法中最流行的選擇策略。在實際應用中此策略比基本的輪盤賭效果要好些。它的策略也很直觀,就是我們在整個種羣中抽取n個個體,讓他們進行競爭(錦標賽),抽取其中的最優的個體。參加錦標賽的個體個數成為tournament size。通常當n=2便是最常使用的大小,也稱作Binary Tournament Selection.
Tournament Selection的優勢:
(1) 更小的複雜度O(n);
(2) 易並行化處理;
(3) 不易陷入局部最優點;
(4) 不需要對所有的適應度值進行排序處理。
下面這張圖是n=3的錦標賽選擇的過程。
選擇結束之後就是crossover(雜交)。雜交又分為一點雜交和多點雜交,如下圖所示。
雜交之後就是變異。
遺傳演算法的整體過程如下所示。選出好的後代之後,將population中fitness小的individuals給替換掉,然後不斷迭代。一般來說,有三種終止條件。
(1) 適應度函數不再提升;
(2) 到達指定的迭代次數;
(3) 適應度函數到達一個指定的值。
在實際應用中,特徵可以當作物品,模型的評估指標可當作適應度函數,通過這種方式來做特徵選擇。下面是代碼。
import pandas as pd import numpy as np from copy import deepcopy from random import random, sample, randint from bisect import bisect_right from itertools import accumulate from sklearn.ensemble import IsolationForest from sklearn.metrics import roc_auc_score import time
def fitness_func(chromosomes, data, ground_truth, model=IsolationForest): """ the function of fitness :param chromosomes: population which have their own set of chromosomes :param data: features :param ground_truth: label :param model: the model of outlier detection :return: fitness """ # chromosomes are binary strings col_arr = np.array(data.columns.tolist()) if not isinstance(chromosomes, np.ndarray): raise ValueError("Chromosomes type must numpy array") news = col_arr[chromosomes > 0] features_new = data[news] clf = model(contamination=0.05) clf.fit(features_new) y_score = clf.decision_function(features_new) auc = roc_auc_score(ground_truth, y_score) return auc
def roulette_wheel_selection(fitness, population): """ Roulette Wheel Selection Algorithm :param fitness: the fitness of each chromosome :param population: chromosomes :return: fathers chromosome and mothers chromosome """ # Normalize fitness min_fit = min(fitness) fitness_new = [(i - min_fit) for i in fitness]
# Create roulette wheel sum_fit = sum(fitness_new) wheel = list(accumulate([i/sum_fit for i in fitness_new]))
# Select a father and a mother. father_idx = bisect_right(wheel, random()) father = population[father_idx] mother_idx = (father_idx + 1) % len(wheel) mother = population[mother_idx]
return father, mother
def tournament_selection(fitness, population, tournament_size=2): """ Tournament Selection Algorithm :param fitness: fitness function :param population: chromosomes :param tournament_size: the size of competitors :return: winners chromosome include father and mother """ competitors_1 = sample(population, tournament_size) competitors_2 = sample(population, tournament_size) father = max(competitors_1, key=lambda x: fitness[population.index(x)]) mother = max(competitors_2, key=lambda x: fitness[population.index(x)]) return father, mother
def crossover(father, mother, method="multi", prob=0.80): """ Crossover of parentschromosomes :param father: fathers chromosome :param mother: mothers chromosome :param method: the method of crossover (default="multi") - multi: multi point crossover -one: one point crossover :param prob: the probability of crossover :return: off-springs chromosomes after crossover """ chromosomes_1 = deepcopy(father) chromosomes_2 = deepcopy(mother)
do_cross = True if random() <= prob else False if do_cross and method == "multi": for i, (g1, g2) in enumerate(zip(chromosomes_1, chromosomes_2)): do_exchange = True if random() < 0.5 else False if do_exchange: chromosomes_1[i], chromosomes_2[i] = g2, g1 if do_cross and method == "one": point = randint(0, len(chromosomes_1)-1) chromosomes_1[point:], chromosomes_2[point:] = chromosomes_2[point:], chromosomes_1[point:] return chromosomes_1, chromosomes_2
def mutation(chromosome, prob=0.50): """ The mutation of chromosome :param chromosome: genome :param prob: the probability of mute :return: chromosome after mute """ do_mutation = True if random() <= prob else False
if do_mutation: for i, gene in enumerate(chromosome): no_flip = True if random() > prob else False if no_flip: continue
chromosome[i] = gene ^ 1
return chromosome
def update_population(chromosome, data, y_true, fitness, population): """ Update the raw population :param chromosome: off-springs :param data: features :param fitness: fitness of population :param population: chromosomes :return: the fitness and population after remove the worse chromosomes """ fit_ = fitness_func(np.array(chromosome), data, y_true) if fit_ > min(fitness): idx = fitness.index(min(fitness)) fitness[idx] = fit_ population[idx] = chromosome return fitness, population
def darwin(features, size=100): """ In memory of Charles Darwin :param features: the features of raw data :param size: the numbers of chromosome :return: good people """ nums = features.shape[1] size_population = size if size_population % 2 != 0: raise ValueError(Population size must be an even number) population = [np.random.randint(0, 2, nums).tolist() for i in range(size_population)] fitness = [] for individuals in population: fit = fitness_func(np.array(individuals), features, y_true) fitness.append(fit)
max_fit = max(fitness) n = 0 iterations = 200 for i in range(iterations): if i % 10 == 0: print(i) if max(fitness) > max_fit: n = 0 max_fit = max(fitness) print("Result get better and the fitness is {}".format(max_fit)) else: n += 1 father_selection, mother_selection = tournament_selection(fitness, population) child_1, child_2 = crossover(father_selection, mother_selection) child_muted_1, child_muted_2 = mutation(child_1), mutation(child_2)
fitness, population = update_population(child_muted_1, features, fitness, population) fitness, population = update_population(child_muted_2, features, fitness, population)
if n > 150 and i < iterations: break print(max(fitness)) best = population[fitness.index(max(fitness))] col_arr = np.array(features.columns.tolist()) evolution = col_arr[np.array(best) > 0] print(evolution) return evolution
if __name__ == "__main__": df = pd.read_csv("features/project.csv") features = df.drop(["project_code", "label"], axis=1)
start = time.time() good_features = darwin(features) end = time.time() print("The evolution is over and the time is {}s".format(round(end-start, 2)))
峯度檢驗是針對單變數進行選擇的方法。
峯度,又稱峯態係數,表徵概率密度分佈曲線在平均值處峯值高低的特徵數。峯度的計算公式如下所示。
峯度包括正態分佈(峯度值等於3),厚尾(峯度值大於3),瘦尾(峯度值小於3)。
一般來說,當一個特徵變數的分佈是厚尾時,可認為該數據分佈含有較多的離羣點。因此在用峯度檢驗做特徵選擇時,會剔除掉峯度值小於3的特徵。
IV值能有效地篩選出合適特徵,對於高維數據,我們可以進行初步篩選,更重要的是,對於離散變數,當前已有的大部分模型可能並不友好(孤立森林,主成分分析法等),可以利用此方法將對應的響應比例或者IV值替代原有變數,以便能夠進行模型訓練。
遺傳演算法通過特徵選擇能使模型輸出符合業務規則的異常,具有可解釋性,不過運行時間較長,可以搭配運行高效的分類器進行建模。
峯度檢驗對假設要求較低,而且完全無監督,能夠快速剔除冗餘特徵,但是不能決定輸出結果是否與業務相關。
https://www.analyticsvidhya.com/blog/2017/07/introduction-to-genetic-algorithm/