[변수 선택] Boruta 와 Lightgbm(rf)을 사용

2020. 4. 24. 20:43분석 Python/Data Preprocessing

728x90

광고 한 번씩 눌러주세요! 블로그 운영에 큰 힘이 됩니다 :)

Boruta는 RandomForest를 사용하여 변수 선택을 하는 함수이다.
하지만 파이썬에서 흔히 알고 있는 sklearn에서는 범주형에 대한 처리를 해주지 않는다.
그래서 Lightgbm은 범주형 변수를 처리할 수 있고  boosting_type을 rf로 하면 가능하다고 생각하여 시작하였다.

 

하지만 지금 현재 Python 버전의 Boruta는 sklearn에 굉장히 특화돼서 만들어졌다. 
그래서 실제로 연속형 변수만 있으면 sklearn에 있는 패키지를 사용하면 되지만, 만약 범주형 변수를 쓴다고 하면 별로 안 좋을 것 같다.

 

 

일단 간략하게 Boruta의 큰 아이디어는 2가지다.

The first idea: shadow features 

일단 첫번째 아이디어는 Permutation을 사용하는 방법이다. 
그래서 shadow feature를 만들고 진짜 데이터와 shadow 데이터를 결합하여 랜덤 포레스트에 적합한다.

그다음에 shadow 변수의 변수 중요도보다 낮은 실제 변수 중요도는 그만큼 쓸모 없다고 판단하는 것이다.

이것은 보통 Permutation Importance 어느정도 들어간 것 같다.

실제 데이터보다 타겟을 맞추는데 더 영향을 줬다면, 이것은 뭔가 쓸모없다 라고 할 수 있다.

이것은 트리 기반이기 때문에 각각에 대해서 split을 하는 것이므로 할 수 있는 것이다.

만약 뉴럴 네트워크나 다른 선형 함수들은 각 변수들 간에 결합이 생길 수 있기 때문에 먼가 이 방법을 쓰면 안 될 것 같다.

import numpy as np
### make X_shadow by randomly permuting each column of X
np.random.seed(42)
X_shadow = X.apply(np.random.permutation)
X_shadow.columns = ['shadow_' + feat for feat in X.columns]
### make X_boruta by appending X_shadow to X
X_boruta = pd.concat([X, X_shadow], axis = 1)

from sklearn.ensemble import RandomForestRegressor
### fit a random forest (suggested max_depth between 3 and 7)
forest = RandomForestRegressor(max_depth = 5, random_state = 42)
forest.fit(X_boruta, y)
### store feature importances
feat_imp_X = forest.feature_importances_[:len(X.columns)]
feat_imp_shadow = forest.feature_importances_[len(X.columns):]
### compute hits
hits = feat_imp_X > feat_imp_shadow.max()

그래서 아래 그림처럼 shadow_height(14)보다 낮은 weight는 먼가 별로일 것 같다?라는 의미로 생각하는 것이다.

 

 

 

The second idea: binomial distribution

그러나 실제 위의 아이디어는 좋지만, 겨우 한번 실행한 것이니 이것은 어떻게 보면 편향이 있을 수 있다.

그래서 두번째 아이디어는 binomial distribution을 사용하는 것이다. 

이러한 편향을 줄이기 위해서는 실험을 많이 하면 된다. 

그래서 두번째 아이디어는 permutation의 seed를 다르게 여러 번 줘서 같은 결과가 나오는지 측정하는 것이다.

### initialize hits counter
hits = np.zeros((len(X.columns)))
### repeat 20 times
for iter_ in range(20):
   ### make X_shadow by randomly permuting each column of X
   np.random.seed(iter_)
   X_shadow = X.apply(np.random.permutation)
   X_boruta = pd.concat([X, X_shadow], axis = 1)
   ### fit a random forest (suggested max_depth between 3 and 7)
   forest = RandomForestRegressor(max_depth = 5, random_state = 42)
   forest.fit(X_boruta, y)
   ### store feature importance
   feat_imp_X = forest.feature_importances_[:len(X.columns)]
   feat_imp_shadow = forest.feature_importances_[len(X.columns):]
   ### compute hits for this trial and add to counter
   hits += (feat_imp_X > feat_imp_shadow.max())

그래서 아래 그림처럼 20번 실험했을 때 age는 전부 중요한 변수로 체크가 된 것을 만들 수 있다.

실제 그러면 binomial(20, 0.5)인 분포를 따르게 되고, 횟수가 많이 나온 것은 그만큼 중요하다고 생각할 수 있고, 
먼가 횟수가 적게 된 것은 별로라고 생각할 수 있다. 여기서 저 점의 색깔의 기준은 binomial distribution은 양 꼬리 0.5%를 의미한다. 

 

 

  • an area of refusal (the red area): the features that end up here are considered as noise, so they are dropped;

 

 

  • an area of irresolution (the blue area): Boruta is indecisive about the features that are in this area;

 

 

  • an area of acceptance (the green area): the features that are here are considered as predictive, so they are kept.

 

그래서 실제 패키지를 사용하면 다음과 같다.

 

!pip install boruta
from boruta import BorutaPy
from sklearn.ensemble import RandomForestRegressor
import numpy as np
###initialize Boruta
forest = RandomForestRegressor(
   n_jobs = -1, 
   max_depth = 5
)
boruta = BorutaPy(
   estimator = forest, 
   n_estimators = 'auto',
   max_iter = 100 # number of trials to perform
)
### fit Boruta (it accepts np.array, not pd.DataFrame)
boruta.fit(np.array(X), np.array(y))
### print results
green_area = X.columns[boruta.support_].to_list()
blue_area = X.columns[boruta.support_weak_].to_list()
print('features in the green area:', green_area)
print('features in the blue area:', blue_area)

As you can see, the features stored in boruta.support_ are the ones that at some point ended up in the acceptance area, thus you should include them in your model.

 

 

하지만 필자는 이것을 범주형이 있으면 sklearn에 randomforest로는 하면 안될 것 같아서 lightgbm에서 boosting_type을 rf로 해서 최대한 비슷한 알고리즘으로 사용하게 하고 여러 번의 테스트를 위해 병렬로 돌릴 수 있게 했다. 

 

from multiprocessing import Pool
import lightgbm as lgb
from functools import partial
def lgb_boruta(iter_,train_x) :
    print(iter_)
    np.random.seed(iter_)
    X_shadow = train_x.apply(np.random.permutation)
    X_boruta = pd.concat([train_x, X_shadow], axis = 1)
    columns = train_x.columns.tolist() + [f"shadow_{i}" for i in train_x.columns.tolist()]
    X_boruta.columns = columns
    boruta_fac_var = fac_var + [f"shadow_{i}" for i in fac_var]
    dtrain = lgb.Dataset(X_boruta,label=train_y,
                         feature_name = columns,
                         categorical_feature = boruta_fac_var)
    param = {'numleaves': 20, 'mindatainleaf': 20,
             'objective':'binary','maxdepth': 5, 
             "boosting": "rf","bagging_freq" : 1 , "bagging_fraction" : 0.8,
             'learningrate': 0.01,"metric": 'auc',
             "lambdal1": 0.1, "randomstate": 133,"verbosity": -1,
             "num_threads" : 1
            }
    lgbmclf = lgb.train(param, dtrain, 500,verbose_eval=-1,
#                         valid_sets=dtrain,
#                         early_stopping_rounds=100,
                         feature_name = columns,
                        categorical_feature= boruta_fac_var)
    importacne = lgbmclf.feature_importance()
    importacne = importacne / importacne.sum()
    feat_imp_X = importacne[:len(train_x.columns)]
    feat_imp_shadow = importacne[len(train_x.columns):]
    value = (feat_imp_X > feat_imp_shadow.max())
    return value
    
n_iter = 100
pool = Pool(n_iter)
result = pool.map(
    partial(lgb_boruta, train_x =train_x ) ,
    np.arange(n_iter))
pool.close()
pool.join()

필자는 100번을 돌리는 테스트를 진행했다.

a = np.array(result).sum(axis=0)
plt.barh(np.arange(len(a)),a)
plt.yticks(np.arange(len(a)),labels = train_x.columns.tolist())
plt.show()

 

왼쪽 : fraud data 오른쪽 loan data

그래서 결과는 다음과 같이 추릴 수 있는 것을 확인했다. 

위의 내용대로라면 100번의 실험을 했으니 95개 이상 선택된 것은 반드시 선택해야 하는 변수고 5개 이하는 버려도 되는 변수 그리고 중간은 분석가의 선택해서 고르면 된다는 뜻이 될 것이다.


그러나 약간 이상하다고 생각하는 것은 것은 연속형 변수에 대해서는 먼가 잘 중요하다고 나오지만, 
범주형 변수는 먼가 중요하지 않다고 하는 경향이 많이 나오는 것을 확인했다.

 

 

https://towardsdatascience.com/boruta-explained-the-way-i-wish-someone-explained-it-to-me-4489d70e154a

 

Boruta explained the way I wish someone explained it to me

Looking under the hood of Boruta, one of the most effective feature selection algorithms

towardsdatascience.com

 

728x90