그동안 Tidymodels를 통해 parsnip 모델과 formula 또는 recipe 전처리기를 포함하는 워크플로 객체를 생성했습니다.
이때, 회귀계수와 같은 파라미터는 훈련 데이터만을 사용하여 값을 추정할 수 있었고 검증 데이터나 테스트 데이터에 대한 성과 지표(MSE, AUC)를 측정해 퍼포먼스가 얼마나 좋은지 판단할 수 있었습니다.
다만 저번 챕터에서 훈련 데이터로는 값을 추정할 수 없는 하이퍼파라미터에 대해 언급하였고 tune 함수를 통해 하이퍼파라미터를 지정할 수 있음을 배웠습니다.
이번 포스팅은 이렇게 지정된 하이퍼파라미터를 튜닝하는 방법 중 하나인 그리드서치(Grid Search)에 대해 소개하는 챕터 13에 대해 알아보려고 합니다.
13 Grid Search | Tidy Modeling with R
The tidymodels framework is a collection of R packages for modeling and machine learning using tidyverse principles. This book provides a thorough introduction to how to use tidymodels, and an outline of good methodology and statistical practice for phases
www.tmwr.org
그리드 서치는 하이퍼 파라미터의 값을 사전에 여러 개 생성하여 성능을 측정하고 제일 좋은 하이퍼 파라미터의 값을 선택하는 방법입니다.
그리드 서치는 Regular grid와 Non-Regular grid 두가지 방식이 있습니다.
Regular grid는 규칙적인 격자라는 뜻 처럼 각 파라미터의 값을 모두 조합하여 생성하는 방식이고
Non-Regular grid는 불규칙한 격자라는 뜻 처럼 모든 파라미터 공간(Parameter Space)에서 임의의 불규칙한 격자를 생성하는 방식입니다.
본격적인 설명에 앞서 준비한 코드부터 설명하도록 하겠습니다.
처음으로 데이터는 아래와 같이 Train-Test 분할 후 Train data에 5 Repeated 10-fold CV를 적용하였습니다.
# Data Split
data(cells)
cells <- modeldata::cells
cells |> ggplot(mapping=aes(x=class, fill=class)) + geom_bar()
cells_split <- cells |> rsample::initial_split(strata = class, prop=0.75)
cells_train <- cells_split |> training()
cells_test <- cells_split |> testing()
cells_folds <- cells_train %>% rsample::vfold_cv(v=10, repeats = 5, strata = class)
아래는 그리드 서치를 설명하기 위한 모델 및 레시피 코드로 본문에서는 MLP 모델을 생성했는데 이번 포스팅에서는 부스팅 모델로 설명하겠습니다. 전처리로는 아래와 같은 사항을 고려했습니다.
- 이진형 반응변수(case)는 데이터 불균형이 심하지 않기때문에 오버샘플링이나 언더샘플링을 적용하지 않음
- 결측치 확인 후 결측치가 없어 Imputation 고려하지 않음
- 설명변수의 스케일 차이가 커서 표준화(평균이 0이고 분산이 1을 따르도록) 설정
- 따로 데이터 분할을 진행하므로 case에 새로운 룰을 부여하여 고려하지 않도록 설정
- 변수의 수가 많아 차원축소 적용 (부스팅에서는 필수적으로 적용하지 않아도 됨)
(주성분 수를 하이퍼파라미터로 설정하였으며 unknown와 finalize 함수를 사용해 데이터에 맞게 자동으로 설정되도록 함)
### Regular and Non-Regular Grids
# Regular Grid : combines each parameter factorially
# Non-Regular Grid : parameter combinations are not formed from a small set of points
xgboost_reipce <-
recipe(formula = class ~ ., data = cells_train) |>
update_role(case, new_role ="splitting variable") |>
step_zv(all_predictors()) |>
step_normalize(all_predictors()) |>
step_pca(all_double_predictors(), num_comp = tune())
xgboost_model <-
boost_tree(trees = tune(), min_n=tune(), tree_depth = tune(),
learn_rate = tune(),
loss_reduction = tune(),
sample_size = tune()) |>
set_mode(mode = "classification") |>
set_engine(engine = "xgboost")
xgboost_workflow <-
workflow() |>
add_recipe(xgboost_reipce) |>
add_model(xgboost_model)
# 하이퍼파라미터 설정
xgboost_param <- xgboost_workflow |> extract_parameter_set_dials()
xgboost_workflow |> extract_parameter_dials("trees") # 1~2000
xgboost_workflow |> extract_parameter_dials("min_n") # 2~40
xgboost_workflow |> extract_parameter_dials("sample_size") # 0.1~1
xgboost_workflow |> extract_parameter_dials("tree_depth") # 1~15
xgboost_workflow |> extract_parameter_dials("num_comp") # 1~4
xgboost_param <- xgboost_param |>
update(trees = trees(range = c(1L, 500L)),
min_n = min_n(range = c(1L, 50L)),
num_comp = num_comp(range = c(0L, unknown()))) |>
finalize(cells_train)
xgboost_param |> extract_parameter_dials("num_comp") # 0~58
1. Regular Grid
Regular Grid는 각 파라미터의 값들을 모두 조합한 격자(Grid)를 의미하며 아래와 같이 다양한 방법으로 생성할 수 있습니다.
tidyr::crossing, tidyr::expand_grid를 통한 생성
- crossing은 중복된 조합을 출력하지 않지만 expand_grid는 같은 값을 가지는 조합을 출력한다.
# Results are same
expand_grid(hidden_units = 1:3,
penalty = c(0.0, 0.1),
epochs = c(100, 200))
crossing(hidden_units = 1:3,
penalty = c(0.0, 0.1),
epochs = c(100, 200))
dials::grid_regular(param, levels)를 통한 생성
- param은 extract_parameter_set_dials의 출력 객체
- levels는 각 하이퍼파라미터 별로 설정할 수준의 수로 파라미터 별로 지정할 수 있으며, 정수 하나만 작성시에는 각 하이퍼파라미터가 가질 수 있는 조합의 수는 levels와 같으며 총 $p^{levels}$의 조합이 생김
xgboost_param |> grid_regular(levels=5)
xgboost_param |> grid_regular(levels=c(3,2,1,1,2,3,5))
- levels = 5로 실행한 결과 7개의 하이퍼 파라미터의 값이 각각 5가지씩, 총 $7^5$개의 조합이 생성되었습니다.
- levels = c(3,2,1,1,2,3,5)로 실행한결과 xgboost_param의 첫번째 파라미터부터 순서대로 3, 2, 1, 1, 2, 3, 5가지의 수준을 생성하여 총 180가지의 조합이 생성되었습니다.
2. Irregular Grid
Irregular Grid는 아래와 같은 방식으로 만들 수 있습니다.
- grid_random(param, size)
- 각 하이퍼파라미터 범위에서 독립적인 다변량 균등 분포를 이용한 샘플링을 적용하여 샘플을 추출하는 방식으로 간단하나, 커버리지가 떨어질 수 있음
(* 변환이 적용되었다면 Transformation scale에서 적용) - size : 총 몇 개의 하이퍼파라미터 값의 조합을 만들지에 대한 인자
- 각 하이퍼파라미터 범위에서 독립적인 다변량 균등 분포를 이용한 샘플링을 적용하여 샘플을 추출하는 방식으로 간단하나, 커버리지가 떨어질 수 있음
- grid_latin_hypercube(param, size) / grid_max_entropy(param, size)
- 공간 채우기(Space-filling) 설계를 통한 샘플링으로 오버래핑(overlapping, 중복)을 최소화 하는 방법
(* grid_latin_hypercube는 라틴 하이퍼큐브 샘플링을 통해 불규칙 그리드를 생성하여 샘플링의 중복성을 줄이고 다양한 조합을 테스트하는 데 유리하며 grid_max_entropy는 최대 엔트로피를 보장하는 설계로, 가능한 한 탐색 공간을 골고루 채우는 방식이며 복잡한 모델에서 성능이 좋습니다.) (추후 grid_tune의 default로 사용됨) - size : 총 몇 개의 하이퍼파라미터 값의 조합을 만들지에 대한 인자
- 공간 채우기(Space-filling) 설계를 통한 샘플링으로 오버래핑(overlapping, 중복)을 최소화 하는 방법
3. Evaluating the Grid
최고의 성능을 보이는 하이퍼파라미터의 조합을 선택하기 위해서 각 조합별로 측정한 성과 지표를 통해 평가해야 할 것 입니다.
위에서 말한 것 처럼, Train data는 모델을 훈련시킬 때 사용하기 때문에 하이퍼파라미터를 평가할 때 사용하는 것은 적절하지 않으며
Test data로 하이퍼파라미터를 평가한다면 하이퍼파라미터가 적합된 모델을 최종 평가할 데이터가 없기 때문에 Test data로 평가하는 것 역시 적절하지 못하므로 Validation data를 만들어서 평가해야 합니다.
# Data Split
data(cells)
cells <- modeldata::cells
cells |> ggplot(mapping=aes(x=class, fill=class)) + geom_bar()
cells_split <- cells |> rsample::initial_split(strata = class, prop=0.75)
cells_train <- cells_split |> training()
cells_test <- cells_split |> testing()
cells_folds <- cells_train %>% rsample::vfold_cv(v=10, repeats = 5, strata = class)
맨 처음에 작성한 코드와 같이 리샘플링 데이터가 준비되었다면 하이퍼파라미터의 최적화를 위한 평가를 tune_grid 함수를 통해 진행 할 수 있습니다.
tune_grid : 성과 지표를 통한 하이퍼파라미터 튜닝 함수
- object : parsnip model specification 또는 workflow object
- preprocessor : model formula 또는 recipe created using recipes::recipe
- resamples : resample object created from rsample function
- param_info :dials::extract_parameter_set_dials의 결과 또는 NULL(다른 인자로부터 자동으로 추출됨)
- grid : data frame of tuning combinations(grid_* 함수도 가능) 또는 positive integer
- metrics : performace metrics created from yardstick::metric_set 또는 NULL(디폴트 성과 지표 사용)
- control : control_grid() object to fine tune the resampling process
저는 아래 코드와 같이 설정하여 튜닝을 진행하였습니다.
library(doMC)
registerDoMC(cores=8)
xgboost_tune <-
tune_grid(xgboost_workflow,
resamples = cells_folds,
metrics = metric_set(roc_auc),
grid = 250,
param_info = xgboost_param,
control = control_grid(verbose=T, allow_par = T))
- grid = 250을 통해 하이퍼파라미터 공간에서 최대 엔트로피 설계 기법을 사용한 250개의 파라미터 조합값을 사용합니다.
- Classificaition 문제이기 때문에, 대표적으로 많이 사용하는 ROC AUC를 사용하여 평가하도록 했습니다.
- [번외] 컴퓨터 사양이 8코어라 코어수를 8개로 설정했습니다.
fit_resamples와 마찬가지로 collect_metrics 함수를 사용할 수 있습니다.
xgboost_tune |> collect_metrics()
xgboost_tune |> collect_metrics(summarize = F)
tune_grid를 통해 튜닝하고 나면 두가지 간편한 함수를 통해 결과를 이해할 수 있습니다.
- autoplot(object, type = "marginals", metric) : 성능 프로파일을 튜닝 파라미터의 값에 따라 표현한 시각화 제공
- show_best(object, n, metric) : metric이 높은 n개의 파라미터 조합을 출력
xgboost_tune |> autoplot(metric = "roc_auc", type = "marginal")
xgboost_tune |> show_best(metric = "roc_auc")
4. Finalizing the Model
그리드 서치를 통해 평가할 하이퍼파라미터 값을 지정하고 tune_grid 함수를 통해 하이퍼파라미터를 튜닝까지 했다면, 어떤 조합에서 성능이 좋은지 파악할 수 있습니다. 하지만 여기서 끝내면 안되고 테스트 데이터에 대해서도 성능이 어느정도 나올지 평가해야 합니다.
tune_grid 함수의 결과는 적절한 하이퍼파라미터의 값을 지표와 함께 제공해 합리적인 선택을 도울 뿐 최종 모델을 적합한 것이 아니므로 select_best라는 함수를 사용해서 성능이 제일 좋게 나온 하이퍼파라미터 값을 사용하도록 finalize_workflow 함수를 통해 지정해야 합니다.
(수동으로 show_best에 나온 파라미터를 확인 후 직접 타이핑해도 상관없습니다.)
### Finalizing the model
final_xgboost_workflow <-
xgboost_tune |> select_best(metric = "roc_auc") |>
finalize_workflow(x=xgboost_workflow)
final_xgboost_workflow |> extract_spec_parsnip()
리샘플링에서 성능이 제일 좋은 하이퍼파라미터 값이 최종 워크플로 모델에 제대로 들어간 것을 확인할 수 있습니다.
이제, 테스트 데이터에 대해 성능을 확인하기 위해 Training set(=Assessment + Analysis)으로 학습을 진행하도록 하겠습니다.
final_xgboost_fit <- final_xgboost_workflow |>
last_fit(split = cells_split)
final_xgboost_fit |> collect_metrics()
last_fit 함수를 통해 training set에 대해 학습을 진행하고 test set에 대해 평가를 진행하였습니다.
최종적으로는 정확도(Accuracy) 기준 83%, ROC-AUC 기준 0.899의 성능을 보이는 모델을 생성하였네요.
5. Tools for Efficient grid search (summary)
1) Submodel Optimization
단일 모델을 한번 학습하더라도 여러 하이퍼파라미터의 값을 평가할 수 있는 모델들이 있습니다.
(eg, Boosting - number of iterations, Regularization model - amount of regularization, PLS - number of PLS components)
tune 패키지는 자동적으로 submodel 최적화가 가능한 모델들이 튜닝될때 여러 하이퍼파라미터의 값을 평가할 수 있도록 지원합니다.
2) Parallel Processing
- tune_grid(parallel_over = "resamples")
논리적 코어가 k개일 때, 각 코어가 하나의 리샘플링 데이터(예: k-fold cross-validation의 각 fold)를 처리하며 병렬 작업을 수행하는 방식
- 장점: 전처리가 복잡한 경우에는 리샘플링된 데이터에 대한 전처리를 병렬로 진행할 수 있어 성능이 좋아집니다.
- 단점: 논리적 코어가 k보다 많은 경우 사용하지 않는 코어가 발생하여 전체 성능이 떨어질 수 있습니다.
또한, 전처리가 간단한 경우에는 이 방식이 오히려 비효율적일 수 있습니다.
- tune_grid(parallel_over = "everything")
논리적 코어가 많을 때 성능을 최적화할 수 있도록 각 코어가 리샘플링 데이터와 하이퍼파라미터 값 쌍을 병렬로 처리하는 방식
- 장점: 논리적 코어가 k보다 훨씬 많으면 이 방식이 더 효율적일 수 있습니다.
- 단점: 리샘플링마다 복잡한 전처리가 필요하다면, 이 방식은 비효율적입니다. 리샘플링 데이터마다 별도로 전처리 후 하이퍼파라미터 튜닝을 진행하는 것이 더 나은 성능을 제공할 수 있습니다.
- 전역 변수 참조시
- 일반적으로 병렬 처리 시, 함수 외부의 전역 변수를 직접 참조하기보다는, 사용할 변수를 함수에 인자로 전달하여 사용하는 것이 성능 면에서 더 효율적임 (rlang으로 하는 동적 변수 참조와 환경 https://moogie.tistory.com/149 참조)
- 객체에 있는 단일 값을 동적으로 전달할 때 !!object 사용
- 객체에 있는 여러 값을 동적으로 전달할 때 !!!object 사용
(* 참고) 코어수 확인 : parallel::detectCores() / future::availableCores()
'Data Science > Modeling' 카테고리의 다른 글
[Tidy Modeling with R] 15. Many Models with Workflow sets (0) | 2024.01.01 |
---|---|
[Tidy Modeling With R] 14. Iterative Search with XGBoost (2) | 2023.12.26 |
[Tidy Modeling with R] 12. 하이퍼파라미터 튜닝 (0) | 2023.10.11 |
[Tidy Modeling with R] 11. Model Comparison (모델 비교) (0) | 2023.10.09 |
[Tidy Modeling with R] 10. Resampling (0) | 2023.10.01 |