mlr3 하이퍼파라미터 최적화

mlr3를 이용한 하이퍼파라미터 튜닝 학습
R
mlr3
machine learning
Published

March 1, 2023

안내사항

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

머신러닝 알고리즘은 보통 파라미터와 하이퍼파라미터를 포함하고 있습니다. 파라미터란 모델의 회귀계수나 가중치처럼 모델을 만들 때 필요한 매개변수입니다. 반면, 하이퍼파라미터는 사용자에 의해 구성됨으로써 파라미터가 어떻게 나올지를 결정합니다.

대표적인 하이퍼파라미터의 예시로는 랜덤포레스트 알고리즘에서 나무의 개수를 정한다던가, 신경망의 학습률을 조정하는 것 등이 있습니다.

하이퍼파라미터는 어떻게 설정하는지에 따라 모델의 성능을 향상시킬 수도, 그 반대가 될 수도 있습니다. 따라서 하이퍼파라미터를 최적화함으로써, 주어진 태스크에 대해 최적의 알고리즘 모델을 개발하는 것이 필요합니다.

어쩌면 최적의 모델을 구성하는 것이 하나의 러너에 하이퍼 파라미터를 다르게 부여하는 벤치마크 실험을 통해 모델을 선택하는 것과 같다고 생각할 수 있습니다. 예를 들어 랜덤포레스트 모델들을 구성하는 나무의 개수를 다르게 정의하여 성능을 비교해본다고 해봅시다.

set.seed(123)
bmr <- benchmark(
  benchmark_grid(
    tasks= tsk("penguins_simple"),
    learners = list(
      lrn("classif.ranger", num.trees = 1, id="1 tree"),
      lrn("classif.ranger", num.trees = 10, id="10 tree"),
      lrn("classif.ranger", num.trees = 100, id="100 tree")
    ),
    resamplings = rsmp("cv", folds=3)
  )
)

autoplot(bmr)

결과를 봤을 때, 나무가 100개로 구성된 랜덤포레스트 모델의 성능이 가장 좋은 것으로 나타났습니다. 다만 이렇게 임의적으로 시행착오를 거쳐 하이퍼파라미터를 조정해주는 것은 많은 시간이 필요한 것은 물론, 종종 편향되고 재생산성이 떨어집니다.

지금까지 개발되어온 정교한 하이퍼파라미터 최적화 방법은 종료(termination) 시점까지 반복적으로 다양한 파라미터를 검토 후, 최적의 하이퍼파라미터 구성을 내놓는 효율적이고 로버스트한 결과를 출력합니다.

모델 튜닝하기

mlr3tuning 패키지를 통해 mlr3 생태계에서 하이퍼파라미터 최적화를 수행할 수 있습니다.

  • TuningInstanceSingleCrit, TuningInstanceMultiCrit: 최적화 결과를 저장하는 튜닝 인스턴스를 만듭니다.

  • Tuner: 최적의 알고리즘을 불러오고 설정할 때 사용.

러너의 학습공간 설정

각각의 러너들은 search space라고 하는 학습공간을 갖습니다. 이 학습공간은 러너들의 하이퍼파라미터 세트를 설정해줄 수 있는 공간이라고 이해할 수 있습니다.

e1071 패키지에 있는 서포트 벡터 머신(SVM)으로 예시를 들어보겠습니다. sonar 데이터를 태스크로 활용하여 SVM 모델을 최적화해봅시다.

lrn_svm <- lrn("classif.svm", type="C-classification", kernel = "radial")

러너의 파라미터 정보는 $param_set 필드에 저장이 됩니다. 여기에는 파라미터의 이름, 클래스의 종류, 레벨, 튜닝 범위 등이 포함되어 있습니다.

as.data.table(lrn_svm$param_set)

범위가 무한대인 파라미터의 경우 이론상으로는 모든 경우의 수를 고려할 수 있으나, 이는 현실적으로 불가능하죠. 여기서는 일부 하이퍼파라미터를 설정해주도록 하겠습니다.

숫자형 (numeric) 하이퍼파라미터의 경우 반드시 하한과 상한의 범위를 지정해줘야 합니다. 이 때 to_tune() 함수를 이용해주면 됩니다.

lrn_svm = lrn("classif.svm",
            cost = to_tune(1e-1, 1e5),
            gamma = to_tune(1e-1, 1),
            type = "C-classification",
            kernel = "radial")

lrn_svm
<LearnerClassifSVM:classif.svm>
* Model: -
* Parameters: cost=<RangeTuneToken>, gamma=<RangeTuneToken>,
  type=C-classification, kernel=radial
* Packages: mlr3, mlr3learners, e1071
* Predict Types:  [response], prob
* Feature Types: logical, integer, numeric
* Properties: multiclass, twoclass

학습공간은 주로 머신러닝 모델 설계자의 경험을 바탕으로 구성되는 것이 일반적입니다. 학습공간과 관련된 자세한 사항은 아래에서 추가적으로 다루도록 하겠습니다.

터미네이터 (Terminator)

앞서 말한 것처럼 이론적으로는 각 러너들의 모든 학습공간을 탐색하며 성능이 가장 뛰어난 모델을 만들 수 있습니다. 하지만 수학적으로, 그리고 현실적으로는 모든 학습공간을 탐색하기란 불가능합니다. 따라서 특정 알고리즘의 하이퍼파라미터 튜닝 과정 도중, 언제 종료할지에 대한 기준 역시 필요합니다.

mlr3tuning 패키지에서는 이것을 Terminator 클래스를 통해 구현해놓았습니다. 터미네이터의 종류는 다음과 같습니다.

Terminator 종류
종 류 설명 사용예시와 초기 파라미터 설정
평가 횟수 특정 탐색횟수가 되면 종료 trm("evals", n_evals= 500)
구동 시간 특정 탐색시간이 되면 종료 trm("run_time", sec= 100)
성능 수준 특정 성능에 도달하면 종료 trm("perf_reached", level= .1)
시간 특정 현실시간이 되면 종료 trm("clock_time", n_evals= 500)
정체 특정 반복동안 개선이 없으면 종료 trm("stagnation", iters= 5, threshold= 1e-5)
조합 여러 터미네이터 조합 trm("combo", terminators = list(run_time_100, evals_200), any = TRUE)

이 중 가장 많이 활용되는 것은 trm("evals", n_evals= 500)trm("run_time", sec= 100) 입니다. trm("combo")는 여러 가지의 터미너이터를 조합하여 종료기준을 설정합니다. any 또는 all을 통해 여러 조건을 하나만 만족해도 종료할지, 모든 기준을 만족해야 할지 설정할 수 있습니다.

ti: 튜닝 인스턴스

튜닝 인스턴스는 어떤 모델을 최적화하기 위해 필요한 모든 정보를 갖는 일종의 환경입니다. 어떤 데이터를 어떤 알고리즘을 통해 학습시킬 건지, 어떤 검증전략을 통해 어떤 성능을 기준으로 파라미터들을 평가할 것인지 등의 정보를 담게 됩니다.

이러한 튜닝 인스턴스는 ti() 함수를 통해 사용자가 직접 설정하거나 tune()을 이용해 자동으로 설정할 수 있습니다. 우선은 ti()를 통해 튜닝 인스턴스를 설정하는 방법부터 알아보겠습니다.

튜닝 인스턴스에는 학습시킬 데이터를 갖고 있는 태스크(task), 러너(learner), 리샘플링 (resampling), 성능 측정(measure), 그리고 터미네이터(terminator)가 사용됩니다.

resampling = rsmp("cv", folds=3)

measure = msr("classif.acc")

learner = lrn("classif.svm",
  cost = to_tune(1e-1, 1e5),
  gamma = to_tune(1e-1, 1),
  kernel = "radial",
  type = "C-classification"
)

terminator = trm("evals")

instance = ti(
  task = tsk("sonar"),
  learner = learner,
  resampling = rsmp("cv", folds=3),
  measures = measure,
  terminator = terminator
)

instance
<TuningInstanceSingleCrit>
* State:  Not optimized
* Objective: <ObjectiveTuning:classif.svm_on_sonar>
* Search Space:
       id    class lower upper nlevels
   <char>   <char> <num> <num>   <num>
1:   cost ParamDbl   0.1 1e+05     Inf
2:  gamma ParamDbl   0.1 1e+00     Inf
* Terminator: <TerminatorEvals>

Tuner

튜닝 인스턴스를 만들었다면, 이제 어떻게 튜닝을 할 것인지를 정해야 합니다. mlr3tuning 에는 다양한 Tuner 클래스가 존재합니다.

mlr3tuning에서 사용가능한 튜닝 알고리즘
Tuner Function call Package
Random Search tnr("random_search") mlr3tuning
Grid Search tnr("grid_search") mlr3tuning
Iterative Racing tnr("irace") mlr3tuning
CMA-ES tnr("cmaes") mlr3tuning
Bayesian Optimization tnr("mbo") mlr3mbo
Hyperband tnr("hyperband") mlr3hyperband
Generalized Simulated Annealing tnr("gensa") mlr3tuning
Nonlinear Optimization tnr("nloptr") mlr3tuning

grid search와 random search는 가장 기본적이면서도 많이 사용되는 튜닝 알고리즘입니다. grid search는 학습공간에서 설정한 범위 내의 모든 하이퍼파라미터를 평가하는 반면, random search는 랜덤하게 학습공간을 탐색하여 하이퍼파라미터들을 평가합니다.

grid search와 random search는 나이브(naive)한 알고리즘으로 평가받는데, 이는 하이퍼파라미터들을 평가할 때, 이전 평가값들을 무시하고 새롭게 하이퍼파라미터들을 구성하기 때문입니다.

반면에 Covariance Matrix Adaptation Evolution Strategy (CMA-ES)나 베이지안 최적화와 같은 보다 정교한 알고리즘들은 모델 기반 최적화라고도 볼립니다. 이 알고리즘들은 하이퍼파라미터 평가 과정 중 이전의 평가된 구성으로부터 학습하여 더 좋은 하이퍼파라미터 조합을 더욱 빠르게 찾아냅니다.

앞서 우리가 만들었던 SVM 알고리즘을 튜너를 활용해 하이퍼파라미터를 탐색해보도록 하겠습니다. tnr() 함수를 이용해 튜닝을 수행하는데, batch_size를 설정하여 한 번에 몇 개의 하이퍼파라미터 구성을 평가할지 설정할 수 있습니다. 이 때 설정해주는 resolution의 경우 하이퍼파라미터 조합의 숫자와 연관이 있습니다. 예를 들어 위의 튜닝 인스턴스에서 범위를 정해준 하이퍼파라미터는 두 개 (cost, gamma)입니다. resolution을 5로 설정할 경우 \(5^2=25\), 즉 25번의 하이퍼파라미터 조합이 구성되는 것입니다.

tuner = tnr("grid_search", resolution = 5, batch_size = 10)
tuner
<TunerGridSearch>: Grid Search
* Parameters: resolution=5, batch_size=10
* Parameter classes: ParamLgl, ParamInt, ParamDbl, ParamFct
* Properties: dependencies, single-crit, multi-crit
* Packages: mlr3tuning

튜닝 수행하기

이제 하이퍼파라미터 튜닝을 실시해보겠습니다. 튜너$optimize(튜닝인스턴스)의 구조로 튜닝을 수행해줍니다.

tuner$optimize(instance)

tune 을 이용한 빠른 튜닝

앞서 ti()를 이용해 튜닝 인스턴스를 설정하고, 터미네이터, 튜너까지 직접 설정해주었는데요. tune() 함수를 이용하면 이러한 튜닝 인스턴스 설정 과정을 간소화할 수 있습니다.

learner = lrn("classif.svm",
  cost  = to_tune(1e-5, 1e5, logscale = TRUE),
  gamma = to_tune(1e-5, 1e5, logscale = TRUE),
  kernel = "radial",
  type = "C-classification"
)

instance = tune(
  tuner = tnr("grid_search", resolution = 5, batch_size = 10),
  task = tsk("sonar"),
  learner = learner,
  resampling = rsmp("cv", folds = 3),
  measures = msr("classif.acc")
)

instance$result

튜닝 결과 분석하기

ti이나 tune 에 관계없이 하이퍼파라미터 튜닝 이후에는 모든 파라미터 구성이 $archive 필드에 포함되어있습니다.

as.data.table(instance$archive)[,.(cost, gamma, classif.acc)]

결과를 보면 25개의 조합별로 성능 점수(classif.acc, 정확도)가 나와있습니다. 또한 튜닝결과에서는 하이퍼파라미터의 조합별 성능 뿐만 아니라 에러나 경고, 학습된 시간, 모델 학습 시간 등의 정보도 확인할 수 있습니다.

as.data.table(instance$archive)[,
  .(timestamp, runtime_learners, errors, warnings)]

마지막으로 하이퍼파라미터 튜닝 결과를 mlr3viz를 이용해 시각화할 수 있습니다.

autoplot(instance, type="surface")

cost와 gamma 의 조합에 따른 성능의 정도가 색상으로 나타났습니다. 정확도가 높을수록 히트맵의 색상이 연하다는 것을 확인할 수 있습니다.

튜닝된 모델 활용하기

튜닝인스턴스에는 튜닝된 결과에 대한 모든 정보를 포함하고 있습니다. instance에 저장된 최적의 파라미터를 활용하여 새로운 러너의 파라미터로 설정해주겠습니다.

svm_tuned = lrn("classif.svm")
svm_tuned$param_set$values = instance$result_learner_param_vals

이제 새로운 러너를 학습하고 결과를 예측할 수 있게 되었습니다.

svm_tuned$train(tsk("sonar"))
svm_tuned$model

Call:
svm.default(x = data, y = task$truth(), type = "C-classification", 
    kernel = "radial", gamma = 0.00316227766016838, cost = 1e+05, 
    probability = (self$predict_type == "prob"))


Parameters:
   SVM-Type:  C-classification 
 SVM-Kernel:  radial 
       cost:  1e+05 

Number of Support Vectors:  93

AutoTuner 로 튜닝 자동화하기

mlr3에서 가장 유용한 기능 중 하나가 AutoTuner 입니다. AutoTuner는 러너를 통해 주어진 하이퍼파라미터를 자동으로 최적화해줌으로써 튜닝과정을 자동으로 수행합니다.

AutoTuner는 러너클래스를 상속받기 때문에 다른 러너들처럼 사용할 수도 있습니다.

learner = lrn("classif.svm",
  cost  = to_tune(1e-5, 1e5, logscale = TRUE),
  gamma = to_tune(1e-5, 1e5, logscale = TRUE),
  kernel = "radial",
  type = "C-classification"
)

at = auto_tuner(
  tuner = tnr("grid_search", resolution = 5, batch_size = 5),
  learner = learner,
  resampling = rsmp("cv", folds = 3),
  measure = msr("classif.ce")
)

at
<AutoTuner:classif.svm.tuned>
* Model: list
* Search Space:
<ParamSet>
       id    class     lower    upper nlevels
   <char>   <char>     <num>    <num>   <num>
1:   cost ParamDbl -11.51293 11.51293     Inf
2:  gamma ParamDbl -11.51293 11.51293     Inf
                                                                                     default
                                                                                      <list>
1: <NoDefault>\n  Public:\n    clone: function (deep = FALSE) \n    initialize: function () 
2: <NoDefault>\n  Public:\n    clone: function (deep = FALSE) \n    initialize: function () 
    value
   <list>
1:       
2:       
Trafo is set.
* Packages: mlr3, mlr3tuning, mlr3learners, e1071
* Predict Type: response
* Feature Types: logical, integer, numeric
* Properties: multiclass, twoclass

이제 at를 활용해 새로운 데이터를 학습하고 검증 데이터를 통해 모델의 성능을 평가할 수 있습니다.

task = tsk("sonar")
split = partition(task)
at$train(task, row_ids = split$train)
at$predict(task, row_ids = split$test)$score(msr("classif.acc"))
classif.acc 
  0.8115942 
lrn_xgb <- lrn('classif.xgboost',
               eta = to_tune(1e-4,1e-2),
               gamma=to_tune(1e-3,1e-2),
               max_depth=to_tune(10,50),
               predict_type='prob'
               )

at <- auto_tuner(
  method=tnr('random_search'),
  learner = lrn_xgb,
  resampling = rsmp('cv',folds=5),
  measure = msr('classif.auc'),
  term_evals = 30
)

at$train(task_sonar)

중첩 리샘플링 Nested resampling

하이퍼파라미터 최적화는 모델 성능 측정 시 발생할 수 있는 편향을 예방하기 위해 추가적인 리샘플링이 필요합니다. 만약 동일한 데이터가 최적의 하이퍼파라미터를 구성하고 모델링 결과를 평가하는 데 사용된다면 매우 심하게 편향이 있을 수 있습니다.

중첩 리샘플링은 추가적인 리샘플링을 수행함으로써 튜닝된 모델의 성능을 평가하는 과정에서 모델 최적화를 분리시켜 놓습니다. 즉 일반적인 방법의 리샘플링 방법으로 모델의 성능을 평가하는 반면, 튜닝의 경우 리샘플링된 데이터를 다시 리샘플링하여 성능을 평가하는 것입니다.

중첩 리샘플링 설명도. 초록 상자는 외부 리샘플링을 위한 3-fold 교차검증(CV), 파란색 박스는 하이퍼파라미터 튜닝을 위한 4-fold 교차 검증 위의 그림은 중첩 리샘플링의 예시를 잘 보여주고 있습니다.

  1. 외부 리샘플링: 각기 다른 훈련, 검증 데이터셋을 만들기 위한 3-fold 리샘플링
  2. 내부 리샘플링: 외부 리샘플링 안에서 하이퍼파라미터 튜닝을 위한 4-fold 리샘플링
  3. 하이퍼파라미터 최적화: 내부 리샘플링 데이터를 활용한 외부 훈련 데이터셋의 하이퍼파라미터 최적화
  4. 모델 학습 (Training): 가장 하이퍼파라미터 최적화가 잘된 외부 학습 데이터셋을 활용해 모델 학습
  5. 모델 평가 (Evaluation):
  6. 교차 검증 (Cross validation): 2~5번 과정을 각 3 fold에서 반복
  7. 집계 (Aggregation): 리샘플링의 성능을 집계하여 평가

중첩 리샘플링은 컴퓨팅 계산이 굉장히 많이 필요합니다. 위에서 예로 들었던 3-fold 외부 리샘플링과 4-fold 내부 리샘플링, 그리고 grid search를 통한 2가지 파라미터를 5개의 resolution 조합으로 구성할 시, \(3*4*5^2 = 300\)번의 모델 학습 및 검증이 이루어지게 됩니다.

AutoTuner를 활용한 중첩 리샘플링

mlr3에선 AutoTuner를 활용한다면 중첩리샘플링도 쉽게 수행이 가능합니다. 코드를 통해 중첩 리샘플링을 살펴보겠습니다.

AutoTuner 내부에서 수행하는 리샘플링은 inner resampling으로 4-fold 교차 검증을 수행합니다. 반면에 at를 대상으로 수행하는 resample()의 경우 outer resampling으로 3-fold 교차 검증을 수행합니다.

learner = lrn("classif.svm",
  cost  = to_tune(1e-5, 1e5, logscale = TRUE),
  gamma = to_tune(1e-5, 1e5, logscale = TRUE),
  kernel = "radial",
  type = "C-classification"
)

at = auto_tuner(
  tuner = tnr("grid_search", resolution = 5, batch_size = 10),
  learner = learner,
  resampling = rsmp("cv", folds = 4),
  measure = msr("classif.ce"),
)

task = tsk("sonar")
outer_resampling = rsmp("cv", folds = 3)

rr = resample(task, at, outer_resampling, store_models = TRUE)

rr
<ResampleResult> with 3 resampling iterations
 task_id        learner_id resampling_id iteration warnings errors
   sonar classif.svm.tuned            cv         1        0      0
   sonar classif.svm.tuned            cv         2        0      0
   sonar classif.svm.tuned            cv         3        0      0

resample()에서 store_models = TRUE 로 설정할 경우, inner tuning 된 AutoTuner 모델들이 저장됩니다. 이를 통해 inner tuning에 대한 정보를 알 수 있습니다.

extract_inner_tuning_results()는 최적의 하이퍼파라미터 구성을 보여주고, extract_inner_tuning_archives()는 모든 하이퍼파리미터 기록을 보여줍니다.

extract_inner_tuning_results(rr)
extract_inner_tuning_archives(rr)

성능 비교

이제, inner tunning과 outer tuning 간의 성능 비교를 통해 모델 과적합 여부를 살펴보겠습니다.

{r} extract_inner_tuning_results(rr)[, .(iteration, cost, gamma, classif.ce)] # inner

{r} rr$score()[,.(iteration, classif.ce)] # outer

외부 리샘플링의 일부 결과에서 확연히 낮은 성능이 나온다는 것은 최적화된 하이퍼파라미터가 데이터에 과적합되었다는 것을 의미합니다. 따라서 튜닝된 모델은 요약계산된 성능으로 보고하는 것이 중요합니다.

rr$aggregate()
classif.ce 
 0.2255349 

마지막으로 중첩 리샘플링을 과도하게 많이 설정하는 경우, 컴퓨터 성능을 많이 필요로 하게 됩니다. 예를 들어 위의 예시에서는 내부 리샘플링 4, 외부 리샘플링 3, 하이퍼파라미터 2개, resolution 5로 인해 \(3*4*5*5=300\), 즉 300번의 모델 훈련 검증을 수행하게 됩니다. 따라서 내부 리샘플링에서는 홀드아웃 등과 같은 방법을 사용하거나 병렬화를 사용하도록 하는 것을 권장합니다.

R6 Class Sugar function 요약
Tuner tnr() 최적화 알고리즘 결정
Terminator trm() 튜닝 알고리즘 종료 시점 기준 설정
TuningInstanceSingleCrit or TuningInstanceMultiCrit ti() 튜닝 세팅 저장 및 결과 저장
AutoTuner auto_tuner() 튜닝과정 자동화
- extract_inner_tuning_results() 중첩 리샘플링의 내부 튜닝결과 추출
- extract_inner_tuning_archives() 중첩 리샘플링의 내부 튜닝 아카이브 추출

클래스 함수 정리

참고자료

https://mlr3book.mlr-org.com/optimization.html

Back to top

Footnotes

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

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