mlr3 피처 선택

머신러닝에서 최적의 피처 선택하기
R
mlr3
machine learning
Author

JYH

Published

April 28, 2023

Important

이 글은 mlr3book1을 참고하여 작성되었습니다. 국내 R 사용자들에게 잘 알려지지 않은 mlr32 패키지를 통해, R에서도 손쉽게 머신러닝을 수행할 수 있다는 것을 보여드리고자 합니다.

인트로

피처 선택 (feature selection)은 크게 두 종류가 있습니다. 먼저 Filter 방식은 데이터 전처리 단계에서 사용됩니다. 반면 Wrapper 방식은 모델을 만들 때 피처의 일부분(subset)을 사용해 성능을 평가할 때 사용되는 방법입니다.

피처 선택은 변수 선택으로도 불리는데, 이 과정은 모델을 학습할 떄 사용될 피처의 일부분을 찾는 과정입니다. 모델 학습 시 최적의 피처들을 사용할 경우 아래와 같은 장점이 있습니다.

  1. 과적합(overfitting) 감소로 인한 성능 향상
  2. 불필요한 feature에 의존하지 않는 안정된(robust) 모델
  3. 모델이 간단해지면서 해석이 용이해짐 4 빠른 모델 학습과 예측
  4. 잠재적으로 값비싼 feature 수집 불필요

필터 방식

필터 방법을 통한 피처 선택은 머신러닝 모델을 학습하기 이전에 수행할 수 있는 전처리(preprocessing) 단계입니다. 예를 들어서 예측하고자 하는 타겟 변수와의 상관계수가 0.2보다 큰 변수들만 선택하는 것입니다.

이러한 방법은 각각의 피처와 타겟 변수에 대한 관계만을 고려하기 때문에 단변량(univariate) 필터라고 합니다. 문제는 이러한 방법이 연속형 변수들로 회귀분석에만 적용할 수 있고, 상관계수가 0.2라는 기준도 임의적이죠.

그래서 피처의 중요성 (feature importance)을 기반으로 하는 다변량 필터 등이 등장을 하게 됩니다. 비록 단변량 필터들은 다변량 필터나 래퍼방식보다 간단하고 빠르게 계산할 수 있다는 장점이 있지만, 성능 측면에서는 같은 고급 필터 방법들이 더 뛰어나곤 합니다.

필터 알고리즘은 각 피처별로 상관계수나 순위와 같은 수치를 부여해 피처를 선택하게 됩니다. 수치가 낮은 리처들은 모델링 단계에서 제외되는 방식입니다.

mlr3에서는 mlr3filters 패키지를 통해 필터 방법을 수행할 수 있습니다. mlr3filters를 통해 수행할 수 있는 필터들은 다음과 같습니다.

  • FilterCorrelation: 숫자 피처 - 숫자 타겟 간 피어슨 또는 스피어만 상관계수 활용
  • FilterInformationGain: 피처의 정보로 인해 감소되는 타겟의 불확실성 정도 활용
  • FilterJMIM: Minimal joint mutual information maximization, 선택된 피처들 간에 겹치는 정보를 최소화
  • FilterPermutation: 주어진 러너에서 각 피처별 순열 피처 중요도를 계산
  • FilterAUC: 각 피쳐별로 ROC 곡선 아래의 공간 계산

그 외에 수행 가능한 모든 필터 방법들은 다음의 사이트를 참고하시기 바랍니다.

Filter value 계산

우선 원하는 필터 방법을 수행하기 위해 R 객체 형태로 만들어주겠습니다. 다른 mlr3 인스턴스와 마찬가지로 딕셔너리(mlr_filters)나 sugar function인 flt()를 통해 선언할 수 있습니다. 각 필터 객체들은 필터 값과 순위를 계산해 내림차순(점수가 높은 순)으로 정렬하는 $calculate() 메소드를 가집니다. 예를 들어 information gain 필터를 사용해보겠습니다.

library(mlr3filters)
library(mlr3)
filter = flt("information_gain")

task = tsk("penguins")
filter$calculate(task)

as.data.table(filter)

penguins 데이터의 경우 species 가 target인데, 펭귄의 종을 설명하는 각 피처들의 점수를 보여줍니다.

일부 filter들은 hyperparameters가 존재하는데, learner에서 param_set을 변경해주는 것처럼 간단히 변경 가능합니다. 예를 들면 correlation 방법의 경우 피어슨 또는 스피어만 방법으로 변경해줄 수 있습니다.

filter_cor <- flt('correlation')
filter_cor$param_set$values <- list(method='spearman')
filter_cor$param_set
<ParamSet>
       id    class lower upper nlevels    default    value
   <char>   <char> <num> <num>   <int>     <list>   <list>
1:    use ParamFct    NA    NA       5 everything         
2: method ParamFct    NA    NA       3    pearson spearman
task2 <- tsk("mtcars")

filter_cor$calculate(task2)
as.data.table(filter_cor)

Feature importance filters

피처 중요도 필터는 중요도(importance) 가 있는 모델에서 사용 가능한 필터입니다. 중요도 계산을 할 수 있는 러너들은 아래와 같습니다.

as.data.table(mlr_learners)[sapply(properties, \(x) "importance" %in% x)]

ranger와 같은 일부 러너는 중요도 속성을 갖고 있지만, learner를 만들 때 지정을 해줘야 합니다.

library(mlr3learners)
lrn_rf <- lrn('classif.ranger', importance='impurity')

이제 FilterImportance 필터를 이용해보겠습니다.

# 결측변수 제거하기
task$filter(which(complete.cases(task$data())))

filter_imp <- flt('importance', learner=lrn_rf)
filter_imp$calculate(task)
filter_imp
<FilterImportance:importance>: Importance Score
Task Types: classif
Properties: -
Task Properties: -
Packages: mlr3, mlr3learners, ranger
Feature types: logical, integer, numeric, character, factor, ordered
          feature     score
1:    bill_length 75.228997
2: flipper_length 50.796965
3:     bill_depth 33.371397
4:         island 25.564683
5:      body_mass 23.717214
6:            sex  1.387641
7:           year  1.005973

Embedded methods

러너로 학습을 진행할 때, 러너는 예측에 도움이 되는 피처의 일부들을 선택하고 나머지 피처는 무시합니다. 이렇게 러너가 선택한 피처의 일부를 피처 선택으로 사용할 수가 있습니다. Embedded 방법이라고 불리는 이유가 바로 피처 선택이 러너 안에 포함되어 있기 때문입니다.

선택된 피처의 경우 러너가 selected_features 라는 속성에 있는 경우 확인해볼 수 있습니다.

task <- tsk('penguins')
learner <- lrn('classif.rpart')

stopifnot('selected_features' %in% learner$properties)

learner$train(task)
learner$selected_features()
[1] "flipper_length" "bill_length"    "island"        
filter <- flt('selected_features', learner=learner)
filter$calculate(task)
filter
<FilterSelectedFeatures:selected_features>: Embedded Feature Selection
Task Types: classif
Properties: missings
Task Properties: -
Packages: mlr3, rpart
Feature types: logical, integer, numeric, factor, ordered
          feature score
1: flipper_length     1
2:    bill_length     1
3:         island     1
4:           year     0
5:     bill_depth     0
6:            sex     0
7:      body_mass     0

결과를 봤을 때, 학습시킨 모델에 의해 선택된 피처 점수는 1, 나머지는 0 즉 사용되지 않은 피처인 것을 알 수 있습니다.

Filter-based feature selection

필터를 통해 각 피처들의 점수가 계산이 되었다면, 다음 모델링 단계에서 피처 선택하여 학습을 진행해야 합니다. 위의 임베디드 방법에서 확인한 selected_features의 경우 1과 0으로 선택되는 변수를 구분할 수 있기 때문에 피처 선택에 있어 가장 직관적입니다. 따라서 여기서 선택된 피처들을 사용한다면 아래와 같이 수행할 수 있습니다.

task <- tsk('penguins')
learner <- lrn('classif.rpart')
filter <- flt('selected_features', learner=learner)
filter$calculate(task)

keep <- names(which(filter$scores==1))
task$select(keep) # column을 선택하기 때문에 select
task$feature_names
[1] "bill_length"    "flipper_length" "island"        

위의 예시에서는 selected_features를 했기 때문에 0과 1로 구분이 되었기 때문에 피처들을 쉽게 선택할 수 있었습니다.

연속형 점수로 피처의 점수가 출력되는 경우에서 피처 선택을 수행하는 경우는 다음과 같은 방법이 있습니다.

  • 위에서 top N개의 feature 선택하는 경우
task <- tsk('penguins')
learner <- lrn('classif.rpart')
filter <- flt('information_gain')
filter$calculate(task)

# top 3개 선택
keep <- names(head(filter$scores,3))
task$select(keep)
  • score가 특정 기준 k 보다 큰 경우
task <- tsk('penguins')
learner <- lrn('classif.rpart')
filter <- flt('information_gain')
filter$calculate(task)

# information gain이 0.5보다 큰 경우
keep <- names(which(filter$scores>0.5))
task$select(keep)

래퍼 방식

래퍼 방식은 피처들의 일부를 선택한 뒤 그 피처들로 모델링을 하고 그 모델의 성능을 비교하여, 최적의 성능을 나타낼 때의 피처들을 선택하게 됩니다. 한마디로 모델의 성능을 최적화하기 위해 반복적으로 피처들을 선택하는 것입니다.

래퍼 방식은 피처들의 순위를 매기는 대신, 일부 피처 사용하여 학습한 뒤 선택된 성능 지표에 따라 평가하게 됩니다. 이는 하이퍼파라미터 최적화 과정과 굉장히 유사합니다. 즉 하이퍼파라미터 최적화를 통해 높은 성능을 내는 하이퍼파라미터를 찾는 것처럼 래퍼 방식을 통해 높은 성능을 내는 피처들을 찾는 것입니다.

간단한 예시

mlr3에서는 mlr3fselect 패키지의 FSelector 객체를 이용하여 위의 방법을 수행합니다.

library(mlr3fselect)
instance <- fselect(
  fselector = fs("sequential"),
  task= tsk('penguins')$select(c("bill_depth","bill_length","body_mass","flipper_length")),
  learner = lrn('classif.rpart'),
  resampling = rsmp('holdout'),
  measure = msr('classif.acc')
)

성능 비교를 위한 피처들의 모든 하위집단을 확인하기 위해선 아래의 코드로 확인 가능합니다.

dt <- as.data.table(instance$archive)
dt[batch_nr ==1, 1:5][order(-classif.acc)]

위의 결과를 확인해보니, flipper_length가 첫 번째 반복에서 가장 높은 예측 성능을 보였습니다(정확도 0.7478). 또한 두 번째 반복에서는 bill_length 피처가 더해지자가 무려 90% 이상의 예측 정확도가 나타났습니다.

dt[batch_nr ==2, 1:5][order(-classif.acc)]
dt[batch_nr ==3, 1:5][order(-classif.acc)]

반면에 세 번째 반복에서 bill_depth가 추가되었지만, 예측 성능이 향상되지는 않았습니다.

최적의 feature 들을 확인하기 위해서는 아래와 같이 입력할 수 있습니다.

instance$result_feature_set

FSelectInstance 클래스

mlr3fselect 에서 피처 선택을 수행할 경우, fselect() 함수는 FSelectInstanceSingleCrit 객체를 만들고, FSelector 객체를 통해 피처 선택을 실시합니다.

FSelectInstanceSingleCrit 객체를 만들기 위해 fsi() sugar function을 사용할 수 있습니다.

instance <- fsi(
  task = tsk("penguins"),
  learner = lrn("classif.rpart"),
  resampling = rsmp("holdout"),
  measure = msr("classif.acc"),
  terminator = trm("evals", n_evals=5)
)

이제 피처 선택을 위한 인스턴스가 만들어졌습니다. 다음으로 해야할 것은 이 인스턴스를 어떤 알고리즘을 통해 수행할 것인지입니다.

Fselector 클래스

mlr3fselect::FSelector에는 다양한 종류의 피처 선택 알고리즘이 존재합니다.

이 알고리즘들을 활용하여 앞서 만들어놓은 인스턴스 (instance)에 대한 피처 선택을 수행합니다. 여기서는 간단한 random search 방식을 이용하도록 하겠습니다. FSelector 클래스는 mlr_fselectors 딕셔너리에서 확인할 수 있습니다. 또한 fs() sugar function을 통해 새로운 피처 선택 클래스를 만들어줄 수 있습니다.

fselector <- fs("random_search")

피처 선택 수행

피처 선택 역시 하이퍼파라미터 최적화에서 수행한 것처럼 $optimize() 메소드로 수행합니다. FSelector 클래스를 실행함으로써 최적의 분류 정확도를 갖는 피처들의 조합을 찾아낼 수 있습니다. 실행해보면 생각보다 오랜 시간이 걸리는 것을 알 수 있습니다.

fselector$optimize(instance)

이 알고리즘은 다음과 같은 흐름으로 진행됩니다.

피처 선택이 종료되었다면, 최적의 피처 세트는 인스턴스를 통해 접근할 수 있습니다.

as.data.table(instance$result)[,.(features, classif.acc)]

FSelectInstanceMultiCrit 을 통해 여러 가지 조건으로 피처 선택을 진행할 수 있습니다. 앞서 만들었던 인스턴스와 달라지는 부분은 measure 뿐입니다. sugar function fsi()을 사용한다면 FSelectInstanceSingleCrit 과 FSelectInstanceMultiCrit을 구분 지을 필요 없이, measure의 개수로 자동으로 정해집니다.

instance = fsi(
  task = tsk("penguins"),
  learner = lrn("classif.rpart"),
  resampling = rsmp("holdout"),
  measure = msrs(c("classif.acc","time_train")),
  terminator = trm("evals", n_evals = 5)
)

피처 선택 자동화

AutoFSelector 클래스는 FSelectInstanc 를 만들고 FSelector 를 만들어 $optimize()하는 과정을 합쳐놓았습니다.

library(mlr3learners)
af = auto_fselector(
  fselector = fs("random_search"),
  learner = lrn("classif.log_reg"),
  resampling = rsmp("holdout"),
  measure = msr("classif.acc"),
  terminator = trm("evals", n_evals=5)
)

AutoFSelector 역시 AutoTuner 와 마찬가지로 러너의 클래스를 상속받기 때문에 러너처럼 활용이 가능합니다. 그래서 피처 선택 이후 $train(),$predict() 메소드 사용이 가능합니다.

이번에는 benchmark()를 통해 피처 선택을 통해 만든 모델과, 그렇지 않은 모델의 성능을 비교해보도록 하겠습니다. sonar 데이터셋을 활용해 로지스틱 회귀 모형을 추가로 만들어줍니다.

grid = benchmark_grid(
  task = tsk("sonar"),
  learner = list(af, lrn("classif.log_reg")),
  resampling = rsmp("cv", folds=3)
)

bmr = benchmark(grid)

결과를 비교했을 때, 비록 모델 학습에 소요된 시간은 AutoFSelector 로 만든 모델이 더 오래 걸렸지만, 정확도 측면에서는 기본 로지스틱 회귀 모델보다 더 높은 성능을 보여주는 것을 알 수 있습니다.

aggr <- bmr$aggregate(msrs(c("classif.acc","time_train")))
as.data.table(aggr)[,.(learner_id, classif.acc, time_train)]

요약

R6 Class Sugar function Summary
Filter flt() 각 피처들의 점수 계산을 통한 피처 선택
FSelectInstanceSingleCrit or FSelectInstanceMultiCrit fselect() 피처 선택 구성 및 결과 저장
FSelector fs() 피처 선택 알고리즘 선택
AutoFSelectoor auto_fselector() 피처 선택 자동화

참고자료

  • https://mlr3book.mlr-org.com/feature-selection.html#sec-multicrit-featsel
Back to top

Footnotes

  1. https://mlr3book.mlr-org.com/↩︎

  2. https://mlr3.mlr-org.com/↩︎