在上一章我們提到可以用遺傳演算法進行特徵選擇,這章就來講一講。

在構建分類模型時,經常需要對自變數進行篩選。特別是在異常檢測領域,特徵選擇是非常重要的,它會讓模型輸出業務上感興趣的異常點,而不是一些無關異常。通常情況下,離羣點往往會體現在幾個具體的特徵維度上,有效的特徵選擇既能降低模型訓練時間,又能保證模型精度。

本文主要介紹三種不同的特徵選擇方法,如下所示。

(1) 評分卡方法,該方法是風控行業常用的模型,利用信息價值(Information Value,以下簡稱IV)和證據權重(Weight of Evidence,以下簡稱WOE)選擇合適的特徵;

(2) 遺傳演算法,利用優化演算法來選擇特徵,選擇標記為1,不選標記為0,通過選擇最優個體、組合交叉和變異,使適應度函數達到最優解,進而得到合適的特徵;

(3) 峯度檢驗,通過對數據自身的分佈和統計量來估計該特徵是否包含異常值。

以上提到的三種方法,前兩種方法要依賴於部分標籤樣本,適合半監督或者監督學習,而第三種方法則完全不需要,適合無監督學習。

IV

IV可以用來衡量自變數的預測能力。類似的指標還有信息增益和基尼係數,IV的計算是以WOE為基礎的。首先來介紹WOE。

WOE是對原始變數的一種編碼形式,要對一個變數進行編碼,首先要對它進行離散化(分箱處理)。分組後,對於第 i 組,WOE的計算公式如下所示。

pleft( y_{i} 
ight) 表示該組中響應客戶(風險模型中,對應的是違約客戶,即正樣本的個體)佔所有樣本中響應客戶的比例,pleft( n_{i} 
ight) 是這個組中未響應客戶佔樣本中所有未響應客戶的比例,# y_{i} 是這個組中響應客戶的數量,# n_{i} 是這個組中未響應客戶的數量,# y_{T} 是樣本中所有響應客戶的數量,# n_{T} 是樣本中所有響應客戶的數量。

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值替代原有變數,以便能夠進行模型訓練。

遺傳演算法通過特徵選擇能使模型輸出符合業務規則的異常,具有可解釋性,不過運行時間較長,可以搭配運行高效的分類器進行建模。

峯度檢驗對假設要求較低,而且完全無監督,能夠快速剔除冗餘特徵,但是不能決定輸出結果是否與業務相關。

參考

analyticsvidhya.com/blo


推薦閱讀:
相關文章