모델 성능 향상을 위해 optuna 쓸 일이 제법 있었는데 이제껏 공식 document를 읽어본 적이 없었다. 그저 구글링으로 코드 짜맞추기를 했을 뿐
그래서 오늘은 autoML 기법 중 하나인 optuna의 document를 읽어보고 이해한 내용을 작성할 것이다.
Optuna란?
2019년 타쿠야 아키바 등에 의해 개발된 하이퍼파라미터 최적화 라이브러리로 여러 종류의 sampler를 제공한다. Grid, random search에 비해 월등히 빠른 탐색속도를 가지며 머신러닝 뿐 아니라 딥러닝(TF, Pytorch)에도 유연하게 적용 가능하다.
간단 동작원리
Optuna tutorial에 나와있는 대로 아주 기본적인 예제를 통해 optuna의 기능을 익혀보고자 한다.
!pip install optuna
import optuna
나는 Colab 환경에서 실험했는데, colab 환경에 자동설치된 라이브러리가 아니므로 install을 먼저 해준다.
def objective(trial):
x = trial.suggest_float("x", -10, 10)
return (x - 2) ** 2
- 먼저 최적화에 사용될 함수의 이름을 objective라고 설정하였다. 함수 이름은 변경해도 되지만, conventionally하게 objective로 설정하는 것이 국룰이다.
- objective 함수에 입력되는 trial은 optuna의 단위 실험 객체를 의미한다. optuna가 파라미터 범위 공간을 한 번 탐색하여 결과를 내면 1trial을 마친 것이다.
- 그 다음 objective 함수 내에 탐색할 인자와 파라미터 공간을 설정해준다. 여기서는 x라는 값에 대하여 [-10, 10] 범위의 float 공간에서의 탐색이 이루어진다.
- 마지막으로 탐색의 목적이 되는 값은 바로 objective function의 output인 $(x-2)^2$이다. Optuna는 trial을 거치면서 $(x-2)^2$을 최소화 하는 x를 찾게된다.
study = optuna.create_study()
study.optimize(objective, n_trials=100)
위에서 설정한 objective를 optimization하기 위해서는 먼저 study object를 설정해주어야 한다. study를 먼저 설정하고, study가 제공하는 optimize를 사용한다고 이해하면 된다.
Trial 16까지의 optimization 결과이다.
best_params = study.best_params
found_x = best_params["x"]
print("Found x: {}, (x - 2)^2: {}".format(found_x, (found_x - 2) ** 2))
100번의 trial결과 optuna가 찾은 최적의 x값은 위와 같다. study에 저장된 best_params를 이용하여 optuna가 찾아낸 최적의 파라미터를 쉽게 확인할 수 있다.
study.best_trial을 이용하여 best_params, best_value, trial number를 한 번에 확인하는 게 가장 편해보인다.
study.trials
for trial in study.trials[:5]:
print(trial)
앞서 학습한 100개의 trial 전부 study.trials에 담겨있다. 언제든 출력해서 결과를 확인할 수 있다.
직접 적용하기
"""
Optuna example that optimizes a classifier configuration for cancer dataset
using XGBoost.
In this example, we optimize the validation accuracy of cancer detection
using XGBoost. We optimize both the choice of booster model and its
hyperparameters.
"""
import numpy as np
import optuna
import sklearn.datasets
import sklearn.metrics
from sklearn.model_selection import train_test_split
import xgboost as xgb
def objective(trial):
(data, target) = sklearn.datasets.load_breast_cancer(return_X_y=True)
train_x, valid_x, train_y, valid_y = train_test_split(data, target, test_size=0.25)
dtrain = xgb.DMatrix(train_x, label=train_y)
dvalid = xgb.DMatrix(valid_x, label=valid_y)
param = {
"verbosity": 0,
"objective": "binary:logistic",
# use exact for small dataset.
"tree_method": "exact",
# defines booster, gblinear for linear functions.
"booster": trial.suggest_categorical("booster", ["gbtree", "gblinear", "dart"]),
# L2 regularization weight.
"lambda": trial.suggest_float("lambda", 1e-8, 1.0, log=True),
# L1 regularization weight.
"alpha": trial.suggest_float("alpha", 1e-8, 1.0, log=True),
# sampling ratio for training data.
"subsample": trial.suggest_float("subsample", 0.2, 1.0),
# sampling according to each tree.
"colsample_bytree": trial.suggest_float("colsample_bytree", 0.2, 1.0),
}
if param["booster"] in ["gbtree", "dart"]:
# maximum depth of the tree, signifies complexity of the tree.
param["max_depth"] = trial.suggest_int("max_depth", 3, 9, step=2)
# minimum child weight, larger the term more conservative the tree.
param["min_child_weight"] = trial.suggest_int("min_child_weight", 2, 10)
param["eta"] = trial.suggest_float("eta", 1e-8, 1.0, log=True)
# defines how selective algorithm is.
param["gamma"] = trial.suggest_float("gamma", 1e-8, 1.0, log=True)
param["grow_policy"] = trial.suggest_categorical("grow_policy", ["depthwise", "lossguide"])
if param["booster"] == "dart":
param["sample_type"] = trial.suggest_categorical("sample_type", ["uniform", "weighted"])
param["normalize_type"] = trial.suggest_categorical("normalize_type", ["tree", "forest"])
param["rate_drop"] = trial.suggest_float("rate_drop", 1e-8, 1.0, log=True)
param["skip_drop"] = trial.suggest_float("skip_drop", 1e-8, 1.0, log=True)
bst = xgb.train(param, dtrain)
preds = bst.predict(dvalid)
pred_labels = np.rint(preds)
accuracy = sklearn.metrics.accuracy_score(valid_y, pred_labels)
return accuracy
if __name__ == "__main__":
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100, timeout=600)
print("Number of finished trials: ", len(study.trials))
print("Best trial:")
trial = study.best_trial
print(" Value: {}".format(trial.value))
print(" Params: ")
for key, value in trial.params.items():
print(" {}: {}".format(key, value))
Optuna github에서 가져온 XGBoost 예제 (optuna-examples/xgboost/xgboost_simple.py at main · optuna/optuna-examples · GitHub)
먼저 학습에 필요한 모든 과정은 objective function 내부에서 진행된다. (train, valid split 까지도! 근데 train, valid split은 그냥 object 밖에서 해도 될 듯.)
그 후 param에 최적화에 필요한 파라미터들을 제공한다. 이때 주의해야 할 점이 있다면 파라미터들의 형식에 따라 다음 포맷을 맞춰주어야 한다.
- optuna.trial.Trial.suggest_categorical() for categorical parameters
- optuna.trial.Trial.suggest_int() for integer parameters
- optuna.trial.Trial.suggest_float() for floating point parameters
모델마다 사용되는 파라미터가 다르고, 해당 파라미터가 어떤 형식으로 입력되는지를 외우고 다닐 수는 없으므로 그때그때 필요한 정보를 모델 공식문서에서 확인하여 설정하면 된다.
또 한 가지 더 주의할 점은 objective의 return값이 loss가 아니라 accuracy로 설정했다는 점이다. optuna의 study는 기본적으로 objective의 출력값을 minimize하는 방향으로 최적화를 수행한다. 하지만 이 경우 objective 출력값을 maximize해야 하므로 study = optuna.create_study(direction="maximize")로 설정하여 accuracy를 maximize하는 방향으로 최적화가 수행되도록 해야 한다.
TF와 Pytorch 딥러닝에도 적용한 example들을 공식 깃허브에서 제공하고 있으므로 필요할 때마다 찾아서 적용하면 되겠다. GitHub - optuna/optuna-examples: Examples for https://github.com/optuna/optuna
Sampler
Optuna에서는 파라미터를 탐색하는 방법론인 sampler를 변경할 수 있다.
기본 설정은 BaseSampler이며 위에 나와있는 Sampler들로 변경할 수 있다. 공식문서에서 각 Sampler에 대한 상세 정보를 확인할 수 있다. (사실 읽어봐도 이해가 잘 안돼서 그냥 기본 BaseSampler 사용하기로 했다.. 또 GridSampler와 RandomSampler는 Graid search, Random search와는 다른 것이라고 한다. Sampler와 Search는 다르다.)
또 TEPSampler는 Bayesian optimization을 사용한다고 하는데 왜 BaseSampler가 기본설정인 것일까? 궁금하구만
Pruner
Pruner는 현재 trial의 성능이 답이 없어보일 때 학습을 빠르게 멈추고 다음 trial로 넘어가는 기능을 한다. Parameter space가 크면 최적 파라미터를 찾는 데 시간이 굉장히 오래 걸리는데 pruner를 사용하여 시간을 대폭 단축할 수 있다.
import logging
import sys
import sklearn.datasets
import sklearn.linear_model
import sklearn.model_selection
def objective(trial):
iris = sklearn.datasets.load_iris()
classes = list(set(iris.target))
train_x, valid_x, train_y, valid_y = sklearn.model_selection.train_test_split(
iris.data, iris.target, test_size=0.25, random_state=0
)
alpha = trial.suggest_float("alpha", 1e-5, 1e-1, log=True)
clf = sklearn.linear_model.SGDClassifier(alpha=alpha)
for step in range(100):
clf.partial_fit(train_x, train_y, classes=classes)
# Report intermediate objective value.
intermediate_value = 1.0 - clf.score(valid_x, valid_y)
trial.report(intermediate_value, step)
# Handle pruning based on the intermediate value.
if trial.should_prune():
raise optuna.TrialPruned()
return 1.0 - clf.score(valid_x, valid_y)
optuna.logging.get_logger("optuna").addHandler(logging.StreamHandler(sys.stdout))
study = optuna.create_study(pruner=optuna.pruners.MedianPruner())
study.optimize(objective, n_trials=20)
해당 포스트에서는 Optuna의 빙산의 일각만 낼름했다. 역시 document는 양이 방대하고 이해하기 굉장히 어렵다 ㅎㅎ. 나중에 몇 번 더 보고 이해한 내용 추가해야겠다. Bye~
'딥러닝' 카테고리의 다른 글
5. 경사하강법, 역전파 알고리즘 (0) | 2023.09.15 |
---|---|
4. 시그모이드와 크로스엔트로피, 양수출력문제, 죽은 ReLU (0) | 2023.09.14 |
3. 활성화 함수 (Activation function) (0) | 2023.09.13 |
2. 분류와 회귀 (0) | 2023.09.13 |
1. 순방향 신경망 (0) | 2023.09.13 |