Optuna で前回の続きから探索するスクリプトと、手元にある結果をインプットして探索するサンプルスクリプトを記します (LightGBM と Iris データセットを使います)。
参考文献
- optuna.study.create_study — Optuna 4.8.0 documentation
- https://docs.sqlalchemy.org/en/21/core/engines.html#sqlite
- https://github.com/optuna/optuna/blob/v4.7.0/pyproject.toml#L38
- optuna.trial.create_trial — Optuna 4.8.0 documentation
前回の続きから探索
optuna.create_study() に storage='sqlite:///optuna.db', study_name='iris' のように指定すると、ワーキングディレクトリ以下に optuna.db という SQLite データベースファイルが作成され、その中に探索名 iris で探索履歴が記録されます。 [1] 次回以降はそこから study を明示的にロードして使用するか、あるいは optuna.create_study() で load_if_exists=True としておけば、前回の続きから探索します。
-
storageはSQLAlchemy形式の URLsqlite:///{相対パス}を指定します。絶対パスで記述したい場合はプラットフォームによります。 [2] なお、optunaインストール時にSQLAlchemyが同梱されています。 [3] - SQLite は Python に標準で同梱されているので (Windows なら以下のようなパスにあることがわかります)、インストール不要です。
C:\Users\Cookie\AppData\Local\Programs\Python\Python314\DLLs\sqlite3.dll
- もちろん、他に
SQLAlchemyから利用可能なデータベースをお持ちの場合は、SQLite でなくそちらを利用しても構いません。 - 同じデータベースファイルでも、探索名を変更すると別の探索として記録されます。
- 探索履歴をクリアするには、データベースファイル自体を削除すればよいです (ただし、同じデータベースファイルに他の探索名も保存している場合、まとめて削除されます)。
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import lightgbm as lgb
import optuna
def get_model(num_leaves, learning_rate):
return lgb.LGBMClassifier(
objective='multiclass', random_state=42,
num_leaves=num_leaves, n_estimators=10,
learning_rate=learning_rate, verbosity=-1,
)
def fit_model(model, x_train, y_train, x_eval, y_eval):
model.fit(
x_train, y_train, eval_set=(x_eval, y_eval),
eval_metric='multi_logloss',
callbacks=[
lgb.early_stopping(stopping_rounds=3),
lgb.log_evaluation(1),
],
)
return model.best_score_['valid_0']['multi_logloss']
if __name__ == '__main__':
data = load_iris(as_frame=True)
data.data.columns = [col.replace(' ', '') for col in data.data.columns]
x_train, x_eval, y_train, y_eval = train_test_split(
data.data, data.target, test_size=0.4, random_state=42,
)
def objective(trial):
model = get_model(
num_leaves=trial.suggest_int('num_leaves', 4, 16),
learning_rate=trial.suggest_float('learning_rate', 0.01, 0.3),
)
return fit_model(model, x_train, y_train, x_eval, y_eval)
study = optuna.create_study(
direction='minimize',
sampler=optuna.samplers.TPESampler(seed=42),
storage='sqlite:///optuna.db', study_name='iris', load_if_exists=True,
)
study.optimize(objective, n_trials=20)
print(f'累計試行数: {len(study.trials)}')
print(f'正常終了数: {len(study.get_trials(states=[optuna.trial.TrialState.COMPLETE]))}')
print(f'最良値: {study.best_value}')
print(f'最良パラメータ: {study.best_params}')
1 回目の実行ではこうなります。
累計試行数: 20
正常終了数: 20
最良値: 0.0700987094047299
最良パラメータ: {'num_leaves': 5, 'learning_rate': 0.29766553445936644}
2 回目の実行ではこうなります。
累計試行数: 40
正常終了数: 40
最良値: 0.06906769631489072
最良パラメータ: {'num_leaves': 7, 'learning_rate': 0.2996420430967456}
また、探索実行中にデータベースファイルから進捗状況を覗きみることもできます。
python -c "import optuna;study = optuna.load_study(storage='sqlite:///optuna.db', study_name='iris');print(f'累計試行数: {len(study.trials)}');print(f'最良値: {study.best_value}')"
手元の結果をインプットして探索
先のように、最初から Optuna のストレージを使っていれば結果を引き継げますが、以下のような場合でも手元の結果を引き継ぎたいこともあると思います。
- 例. Optuna ストレージに保存していなかったが、実行ログなら手元にある
- 例. Optuna 探索結果ではないが、いくつかのパラメータセットでの結果が手元にある
このような場合でも、手元の結果を optuna.trial.create_trial() によって Optuna 試行結果として study にインプットすることができます。以下はログ文字列から、どんなパラメータのときどんな値だったかをパースしてインプットする例です (先のスクリプトへの追記)。
- 試行結果を作成するとき、
distributions(探索空間) を指定しなければなりません (省略するとエラーになります)。手元の結果が Optuna 探索結果でない場合は、探索空間などないかもしれませんが、これから探索する空間を設定します。
from optuna.distributions import IntDistribution, FloatDistribution
from optuna.trial import TrialState
import ast
import re
info = """
Trial 0 finished with value: 0.0737680660456897 and parameters: {'num_leaves': 8, 'learning_rate': 0.28570714885887566}.
Trial 1 finished with value: 0.1399609424143498 and parameters: {'num_leaves': 13, 'learning_rate': 0.18361096041714062}.
Trial 2 finished with value: 0.5137710589438976 and parameters: {'num_leaves': 6, 'learning_rate': 0.055238410897498764}.
Trial 3 finished with value: 0.0852868252192037 and parameters: {'num_leaves': 4, 'learning_rate': 0.2611910822747312}.
Trial 4 finished with value: 0.1149491156703891 and parameters: {'num_leaves': 11, 'learning_rate': 0.21534104756085318}.
Trial 5 finished with value: 0.07098999758054785 and parameters: {'num_leaves': 4, 'learning_rate': 0.29127385712697834}.
Trial 6 finished with value: 0.4194163183039963 and parameters: {'num_leaves': 14, 'learning_rate': 0.07157834209670008}.
Trial 7 finished with value: 0.46533052899027005 and parameters: {'num_leaves': 6, 'learning_rate': 0.06318730785749581}.
Trial 8 finished with value: 0.1652655502419692 and parameters: {'num_leaves': 7, 'learning_rate': 0.16217936517334897}.
Trial 9 finished with value: 0.32180736956582634 and parameters: {'num_leaves': 9, 'learning_rate': 0.09445645065743215}.
Trial 10 finished with value: 0.10241990975835881 and parameters: {'num_leaves': 16, 'learning_rate': 0.23316708394781974}.
Trial 11 finished with value: 0.07292290888362052 and parameters: {'num_leaves': 4, 'learning_rate': 0.28699163142257256}.
Trial 12 finished with value: 0.07931955083057898 and parameters: {'num_leaves': 4, 'learning_rate': 0.2732227906319301}.
Trial 13 finished with value: 0.07010441394638074 and parameters: {'num_leaves': 4, 'learning_rate': 0.2961186783563627}.
Trial 14 finished with value: 0.2452646213410311 and parameters: {'num_leaves': 11, 'learning_rate': 0.12008523059186482}.
Trial 15 finished with value: 0.10398841344754403 and parameters: {'num_leaves': 6, 'learning_rate': 0.22674431431851044}.
Trial 16 finished with value: 0.0700987094047299 and parameters: {'num_leaves': 5, 'learning_rate': 0.29766553445936644}.
Trial 17 finished with value: 0.12786719806429075 and parameters: {'num_leaves': 8, 'learning_rate': 0.19472171449449766}.
Trial 18 finished with value: 0.09178277267552439 and parameters: {'num_leaves': 6, 'learning_rate': 0.2504775299557142}.
Trial 19 finished with value: 0.22790372856392457 and parameters: {'num_leaves': 5, 'learning_rate': 0.12752091976610797}.
"""
def input_trials(study):
distributions = {
'num_leaves': IntDistribution(4, 16),
'learning_rate': FloatDistribution(0.01, 0.3),
}
for line in info.strip().splitlines():
m = re.search(r'value: ([0-9.eE+-]+) and parameters: ({.*})\.', line)
trial = optuna.trial.create_trial(
state=TrialState.COMPLETE,
value=float(m.group(1)), # Ex. 0.0737680660456897
params=ast.literal_eval(m.group(2)), # Ex. {'num_leaves': 8, 'learning_rate': 0.28570714885887566}
distributions=distributions,
)
study.add_trial(trial)
上記の関数をコールしてから探索を 20 回実行すると、ストレージから探索結果をロードしたときと同じ最良値にたどりつくことができます。
study = optuna.create_study(
direction='minimize',
sampler=optuna.samplers.TPESampler(seed=42),
# storage='sqlite:///optuna.db', study_name='iris', load_if_exists=True,
)
input_trials(study)
study.optimize(objective, n_trials=20)
print(f'累計試行数: {len(study.trials)}')
print(f'正常終了数: {len(study.get_trials(states=[optuna.trial.TrialState.COMPLETE]))}')
print(f'最良値: {study.best_value}')
print(f'最良パラメータ: {study.best_params}')
累計試行数: 40
正常終了数: 40
最良値: 0.06906769631489072
最良パラメータ: {'num_leaves': 7, 'learning_rate': 0.2996420430967456}
備考
- この記事の方法 (前回の続きから探索 / 手元の結果をインプットして探索) によって「20 回で中断していた探索を再開して 20 回探索」した場合、「連続して 40 回探索」した場合とは到達点がずれます。サンプラー (上記では
optuna.samplers.TPESampler) の状態がもはや異なってくるためです (と思っています)。もし到達点を合わせたいなら、- 「20 回分の試行結果をロード (インプット) したら、サンプラーも 20 回分の試行を終えた状態に更新する」とする必要があると思います。20 回試行するとき同様にサンプラーのメソッドを叩けばできそうな気がしますが、やってみたことはありません。
- 自分で状態管理できる自前のサンプラーを用意すればよいかもしれません。
- あるいは逆に「常に 10 回ごとに中断する」などとすればよいかもしれません。