Optuna とは
機械学習のモデルには、いくつかのハイパーパラメータがあり、そのハイパーパラメータの設定次第で精度が大きく変化します。最適なハイパーパラメータを探すタスクをハイパーパラメータチューニングと言います。ハイパーパラメータチューニングには、次のようなハイパーパラメータチューニング用の探索アルゴリズムが提案されています。
- グリッドサーチ(Grid Search)
- ランダムサーチ(Random Search)
- ベイズ最適化(Bayesian Optimization)
グリッドサーチでは、設定した範囲内でハイパーパラメータの全組み合わせを試行します。ランダムサーチでは、ランダムにハイパーパラメータを組み合わせて試行します。ベイズ最適化では前回のハイパーパラメータの組み合わせによる試行を元にに効率的にハイパーパラメータの組み合わせを探索していきます。
OptunaとはハイパーパラメータチューニングのPythonフレームワークです。主にベイズ最適化の一種であるTPE(Tree-structured Parzen Estimator)と呼ばれるアルゴリズムを使って最適な値の探索してくれます。
Optuna の用語
Optunaには次のような用語があります。
- Study: 最適化の一連の試行
- Trial: 目的関数の実行1回分の試行
Optuna の使い方
まずはOptunaをインストールします。
$ pip install optuna
大きく次の3ステップでOptunaによる最適化を行うことができます。
- 目的関数をラップする
objective
関数を定義する Study
型の変数を生成するoptimize
メソッドで最適化する
以下は、(x - 2) ** 2
を最小にするx
を探索するコードになります。
import optuna
# step 1
def objective(trial: optuna.Trial):
x = trial.suggest_uniform('x', -10, 10)
score = (x - 2) ** 2
print('x: %1.3f, score: %1.3f' % (x, score))
return score
# step 2
study = optuna.create_study(direction="minimize")
# step 3
study.optimize(objective, n_trials=100)
study.best_value
には最小となる(x - 2) ** 2
が格納されています。
>> study.best_value
0.00026655993028283496
study.best_params
には最小となる(x - 2) ** 2
のときのx
のパラメータ、つまりx
が格納されています。
>> study.best_params
{'x': 2.016326663170496}
study.best_trial
には最小となる(x - 2) ** 2
のときの試行の内容が格納されています。
>> study.best_trial
FrozenTrial(number=46, state=TrialState.COMPLETE, values=[0.00026655993028283496], datetime_start=datetime.datetime(2023, 1, 20, 11, 6, 46, 200725), datetime_complete=datetime.datetime(2023, 1, 20, 11, 6, 46, 208328), params={'x': 2.016326663170496}, user_attrs={}, system_attrs={}, intermediate_values={}, distributions={'x': FloatDistribution(high=10.0, log=False, low=-10.0, step=None)}, trial_id=46, value=None)
study.trials
には行われた試行の内容が格納されています。
>> study.trials
[FrozenTrial(number=0, state=TrialState.COMPLETE, values=[48.70102052494164], datetime_start=datetime.datetime(2023, 1, 20, 6, 4, 39, 240177), datetime_complete=datetime.datetime(2023, 1, 20, 6, 4, 39, 254344), params={'x': 8.978611647379559}, user_attrs={}, system_attrs={}, intermediate_values={}, distributions={'x': FloatDistribution(high=10.0, log=False, low=-10.0, step=None)}, trial_id=0, value=None),
.
.
.
FrozenTrial(number=99, state=TrialState.COMPLETE, values=[1.310544492087495], datetime_start=datetime.datetime(2023, 1, 20, 6, 4, 40, 755667), datetime_complete=datetime.datetime(2023, 1, 20, 6, 4, 40, 763725), params={'x': 0.8552098480125299}, user_attrs={}, system_attrs={}, intermediate_values={}, distributions={'x': FloatDistribution(high=10.0, log=False, low=-10.0, step=None)}, trial_id=99, value=None)]
Trial の設定
どのパラメータをどのように最適化するかの設定は次のように記述します。
optimizer = trial.suggest_categorical('optimizer', ['MomentumSGD', 'Adam'])
num_layers = trial.suggest_int('num_layers', 1, 3)
dropout_rate = trial.suggest_uniform('dropout_rate', 0.0, 1.0)
learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-2)
drop_path_rate = trial.suggest_discrete_uniform('drop_path_rate', 0.0, 1.0, 0.1)
Optunaが提供しているTrialのメソッドは次のとおりです。
メソッド | 説明 |
---|---|
suggest_categorical (name, choices) |
Suggest a value for the categorical parameter. |
suggest_discrete_uniform (name, low, high, q) |
Suggest a value for the discrete parameter. |
suggest_float (name, low, high, [, step, log]) |
Suggest a value for the floating point parameter. |
suggest_int (name, low, high[, step, log]) |
Suggest a value for the integer parameter. |
suggest_loguniform (name, low, high) |
Suggest a value for the continuous parameter. |
suggest_uniform (name, low, high) |
Suggest a value for the continuous parameter. |
関数の引数の内容は以下になります。
- name: ハイパーパラメータの名前
- low: パラメータが取り得る範囲の最小値
- high: パラメータが取り得る範囲の最大値
- step: パラメータが取り得る値の間隔
- q: 離散化の間隔
- log: パラメータを対数の定義域からサンプリングする場合はTrue
- choices: パラメータのカテゴリ値のリスト
Optuna の便利な機能
Optunaには次の便利な機能があります。
- Pruner
- 分散最適化
- ダッシュボード機能
Pruner
OptunaにはPrunerという見込みの薄いトライアルを自動で中断することができる機能があります。Prunerは次のように記述します。
study = optuna.create_study(
pruner=optuna.pruners.MedianPruner(),
)
上記のコードではMedianPruner()
というPrunerを指定していますが、他のPrunerも存在します。詳細は次の公式ドキュメントをご参照ください。
分散最適化
create_study
の引数にstudy_name
とstorage
を指定することで、トライアル履歴をプロセス間で共有できるようになり、分散処理の実装も容易になります。ストレージを特に指定しない場合は最適化の状況はメモリ内に保持されるため、実行の度にまっさらな状態から探索が始まります。
study = optuna.create_study(
study_name="example-study",
storage="sqlite://example.db",
load_if_exists=True
)
load_if_exists
をTrue
にすることで、すでにDBに同名のStudyが存在する場合の読み込みと再開を許可することもできます。
ダッシュボード機能
Optunaはダッシュボード機能を提供しており、探索の進行状況を確認することができます。
PyTorch モデルの最適化
次の例では、PyTorchとFashionMNISTを用いたファッション商品認識の検証精度を最適化しています。
import optuna
from optuna.trial import TrialState
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data
from torchvision import datasets
from torchvision import transforms
DEVICE = torch.device("cpu")
BATCHSIZE = 128
CLASSES = 10
DIR = os.getcwd()
EPOCHS = 10
N_TRAIN_EXAMPLES = BATCHSIZE * 30
N_VALID_EXAMPLES = BATCHSIZE * 10
def define_model(trial: optuna.Trial):
# We optimize the number of layers, hidden units and dropout ratio in each layer.
n_layers = trial.suggest_int("n_layers", 1, 3)
layers = []
in_features = 28 * 28
for i in range(n_layers):
out_features = trial.suggest_int("n_units_l{}".format(i), 4, 128)
layers.append(nn.Linear(in_features, out_features))
layers.append(nn.ReLU())
p = trial.suggest_float("dropout_l{}".format(i), 0.2, 0.5)
layers.append(nn.Dropout(p))
in_features = out_features
layers.append(nn.Linear(in_features, CLASSES))
layers.append(nn.LogSoftmax(dim=1))
return nn.Sequential(*layers)
def get_mnist():
# Load FashionMNIST dataset.
train_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST(DIR, train=True, download=True, transform=transforms.ToTensor()),
batch_size=BATCHSIZE,
shuffle=True,
)
valid_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST(DIR, train=False, transform=transforms.ToTensor()),
batch_size=BATCHSIZE,
shuffle=True,
)
return train_loader, valid_loader
def objective(trial: optuna.Trial):
# Generate the model.
model = define_model(trial).to(DEVICE)
# Generate the optimizers.
optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "RMSprop", "SGD"])
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
optimizer = getattr(optim, optimizer_name)(model.parameters(), lr=lr)
# Get the FashionMNIST dataset.
train_loader, valid_loader = get_mnist()
# Training of the model.
for epoch in range(EPOCHS):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
# Limiting training data for faster epochs.
if batch_idx * BATCHSIZE >= N_TRAIN_EXAMPLES:
break
data, target = data.view(data.size(0), -1).to(DEVICE), target.to(DEVICE)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
# Validation of the model.
model.eval()
correct = 0
with torch.no_grad():
for batch_idx, (data, target) in enumerate(valid_loader):
# Limiting validation data.
if batch_idx * BATCHSIZE >= N_VALID_EXAMPLES:
break
data, target = data.view(data.size(0), -1).to(DEVICE), target.to(DEVICE)
output = model(data)
# Get the index of the max log-probability.
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
accuracy = correct / min(len(valid_loader.dataset), N_VALID_EXAMPLES)
trial.report(accuracy, epoch)
# Handle pruning based on the intermediate value.
if trial.should_prune():
raise optuna.exceptions.TrialPruned()
return accuracy
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100, timeout=600)
pruned_trials = study.get_trials(deepcopy=False, states=[TrialState.PRUNED])
complete_trials = study.get_trials(deepcopy=False, states=[TrialState.COMPLETE])
print("Study statistics: ")
print(" Number of finished trials: ", len(study.trials))
print(" Number of pruned trials: ", len(pruned_trials))
print(" Number of complete trials: ", len(complete_trials))
print("Best trial:")
trial = study.best_trial
print(" Value: ", trial.value)
print(" Params: ")
for key, value in trial.params.items():
print(" {}: {}".format(key, value))
Study statistics:
Number of finished trials: 100
Number of pruned trials: 64
Number of complete trials: 36
Best trial:
Value: 0.8484375
Params:
n_layers: 1
n_units_l0: 77
dropout_l0: 0.2621844457931539
optimizer: Adam
lr: 0.0051477826780949205
LightGBM の最適化
次の例では、LightGBMを用いた癌検出の検証精度を最適化しています。
import numpy as np
import optuna
import lightgbm as lgb
import sklearn.datasets
import sklearn.metrics
from sklearn.model_selection import train_test_split
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 = lgb.Dataset(train_x, label=train_y)
param = {
"objective": "binary",
"metric": "binary_logloss",
"verbosity": -1,
"boosting_type": "gbdt",
"lambda_l1": trial.suggest_float("lambda_l1", 1e-8, 10.0, log=True),
"lambda_l2": trial.suggest_float("lambda_l2", 1e-8, 10.0, log=True),
"num_leaves": trial.suggest_int("num_leaves", 2, 256),
"feature_fraction": trial.suggest_float("feature_fraction", 0.4, 1.0),
"bagging_fraction": trial.suggest_float("bagging_fraction", 0.4, 1.0),
"bagging_freq": trial.suggest_int("bagging_freq", 1, 7),
"min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
}
gbm = lgb.train(param, dtrain)
preds = gbm.predict(valid_x)
pred_labels = np.rint(preds)
accuracy = sklearn.metrics.accuracy_score(valid_y, pred_labels)
return accuracy
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)
[I 2023-01-20 09:18:25,197] A new study created in memory with name: no-name-26038220-3fba-4ada-9237-9ad9e0a7eff4
[I 2023-01-20 09:18:25,278] Trial 0 finished with value: 0.951048951048951 and parameters: {'lambda_l1': 3.6320373475789714e-05, 'lambda_l2': 0.0001638841686303377, 'num_leaves': 52, 'feature_fraction': 0.5051855392259837, 'bagging_fraction': 0.48918754678745996, 'bagging_freq': 4, 'min_child_samples': 30}. Best is trial 0 with value: 0.951048951048951.
.
.
.
[I 2023-01-20 09:18:37,148] Trial 99 finished with value: 0.972027972027972 and parameters: {'lambda_l1': 4.921752856772178e-06, 'lambda_l2': 5.0633857392202624e-08, 'num_leaves': 28, 'feature_fraction': 0.48257699231443446, 'bagging_fraction': 0.7810382257111896, 'bagging_freq': 3, 'min_child_samples': 28}. Best is trial 36 with value: 0.993006993006993.
print(f"Number of finished trials: {len(study.trials)}")
print("Best trial:")
trial = study.best_trial
print(f" Value: {trial.value}")
print(" Params: ")
for key, value in trial.params.items():
print(f" {key}: {value}")
Number of finished trials: 100
Best trial:
Value: 0.993006993006993
Params:
lambda_l1: 2.2820624207211886e-06
lambda_l2: 4.100655307616414e-08
num_leaves: 253
feature_fraction: 0.6477416602072985
bagging_fraction: 0.7393534933706116
bagging_freq: 5
min_child_samples: 36
LightGBM Tuner
LightGBMに限り、OptunaはLightGBM Tunerというものを提供しています。LightGBM Tunerを使うとより簡単にLightGBMのチューニングをすることができます。
ただし、LightGBM Tunerは次のハイパーパラメータのみをチューニングの対象としています。
lambda_l1
lambda_l2
num_leaves
feature_fraction
bagging_fraction
bagging_freq
min_child_samples
import numpy as np
import optuna.integration.lightgbm as lgb
from lightgbm import early_stopping
from lightgbm import log_evaluation
import sklearn.datasets
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
if __name__ == "__main__":
data, target = sklearn.datasets.load_breast_cancer(return_X_y=True)
train_x, val_x, train_y, val_y = train_test_split(data, target, test_size=0.25)
dtrain = lgb.Dataset(train_x, label=train_y)
dval = lgb.Dataset(val_x, label=val_y)
params = {
"objective": "binary",
"metric": "binary_logloss",
"verbosity": -1,
"boosting_type": "gbdt",
}
model = lgb.train(
params,
dtrain,
valid_sets=[dtrain, dval],
callbacks=[early_stopping(100), log_evaluation(100)],
)
prediction = np.rint(model.predict(val_x, num_iteration=model.best_iteration))
accuracy = accuracy_score(val_y, prediction)
best_params = model.params
print("Best params:", best_params)
print(" Accuracy = {}".format(accuracy))
print(" Params: ")
for key, value in best_params.items():
print(" {}: {}".format(key, value))
Best params: {'objective': 'binary', 'metric': 'binary_logloss', 'verbosity': -1, 'boosting_type': 'gbdt', 'feature_pre_filter': False, 'lambda_l1': 3.9283033758323693e-07, 'lambda_l2': 0.11914982777201996, 'num_leaves': 4, 'feature_fraction': 0.4, 'bagging_fraction': 0.46448877892449625, 'bagging_freq': 3, 'min_child_samples': 20}
Accuracy = 0.9790209790209791
Params:
objective: binary
metric: binary_logloss
verbosity: -1
boosting_type: gbdt
feature_pre_filter: False
lambda_l1: 3.9283033758323693e-07
lambda_l2: 0.11914982777201996
num_leaves: 4
feature_fraction: 0.4
bagging_fraction: 0.46448877892449625
bagging_freq: 3
min_child_samples: 20
その他の最適化の例
次のGitHubに他のモデルでのOptunaの実装例が豊富に示されています。
参考