LoginSignup
8
5

More than 1 year has passed since last update.

Optunaの10Tips、かゆいところ、よぉ分からんとこ、諸々

Last updated at Posted at 2022-10-08

1. はじめに

ちょくちょくOputna使っていたら、何やら、Tutorialがいまいちなところ、よく分からんとこ、かゆいところ、いくつか見つかってきた。ので、備忘のため、逆引きの形で、そのTipsをまとめてみました。

(※) 自動最適化、ベイズ最適化、Optunaの初心者の方々に向けた内容です。Kagglerで、機械学習のハイパーパラメータの最適化に飽きてきた方々にもおすすめでございます。あと、実験/解析の条件が多すぎて、考えるの疲れてきた方々にも、見てもらいたい内容ですね。

(※) Optunaは、機械学習のハイパーパラメータやブラックボックスの関数を、自動的に、最適化するためのフレームワークです。詳細は、こちらをご参照下さいませ。最近の流行りの最適化手法を手軽に使えてしまう、素晴らしいソフトなので、ぜひ一度触ってみて下さい。世界で認められている、稀有な国産のソフトです。Qiitaの住人が使ってあげなくてどうする!?と言いたい!!

中身知らなくても使えるぞ!まずは、触ってみよう!!やる事はコピペして実行するだけ!!!階段を登るように技術を吸収していこう♪♪

コピペもめんどくなってきたら、これCopy & edit notebookして、Run Allしてみよう!!一発で全部動きますよい。関連する資料のリンクなんかもあります。英語だけど、内容はほぼほぼ同じです。LightGBMのScikit-learn APIのハイパーパラメータの最適化のもあります。あわせて、ご覧あれ!!

2. 10Tips 一覧

  • #001 あらかじめ分かっている知見を反映して、効率的に、最適化する。
  • #002 決めた回数だけ最適化する。
  • #003 決めた時間内に試せる回数だけ最適化する。
  • #004 最適化した結果を表示する。
  • #005 最適化の履歴をプロットする。
  • #006 目的関数の分布をプロットする。
  • #007 パラメータの重要度をプロットする。
  • #008 目的関数とパラメータの値の関係をプロットする。
  • #009 最適化の履歴を保存する。
  • #010 保存した最適化の履歴から、再び最適化を始める。

3. では本題!!

ここでは、Optunaを使って、ものすごく簡単な関数、
$$ f(x, y) = x^2 + y^2 $$
の最小値を求めてみたいと思います。その中で、OptunaのTipsを紹介していきます。

#000 まずは準備。

まず、必要なライブラリをインポートします。

# Import required libs.
import numpy as np
import optuna
import pickle
import plotly.graph_objects as go
import itertools

次に、最適化する関数(=目的関数)や関連した関数を定義します。

def objective(trial):
    """目的関数を定義する。ここで、パラメータx, yを探索する範囲を指定する。ここでは、例として、`-5 < x, y < 5と`を指定。"""
    x = trial.suggest_float(name="x", low=-5.0, high=5.0, step=0.1)
    y = trial.suggest_float(name="y", low=-5.0, high=5.0, step=0.1)
    return _sphereFunction(x=x, y=y)

def _sphereFunction(x, y):
    """f(x, y) = x^2 + y^2の値を返す関数を定義する。目的関数を、簡単に、変えられるように、このような形にしている。"""
    return pow(x, 2) + pow(y, 2)

def plotSurface(objective, x_range_step, y_range_step, eps=0.001):
    """問題なく、目的関数が定義できていることを確認するため、目的関数をプロットする関数も用意する。"""
    # Prepare data for plot.
    start, stop, step = x_range_step
    x = np.arange(start, stop + eps, step)
    start, stop, step = y_range_step
    y = np.arange(start, stop + eps, step)
    z = np.array([[objective(optuna.trial.FixedTrial({"x": x_, "y": y_})) for x_ in x] for y_ in y])
    
    # Create figure object and plot.
    fig = go.Figure()
    trace = go.Surface(x=x, y=y, z=z,contours_z = dict(show=True, project_z=True))
    fig.add_trace(trace)
    fig.show()

確認のため、目的関数をプロットします。

# Plot 3D surface of objective function for confirmation.
plotSurface(objective=objective, x_range_step=(-5, 5, 0.1), y_range_step=(-5, 5, 0.1))

surface.png

目的関数の定義に問題がないことが分かったので、Studyオブジェクトを作ります。

# Create study for function optimization.
study = optuna.create_study()

これで、準備は完了です。

#001 あらかじめ分かっている知見を反映して、効率的に、最適化する。

まず、Studyオブジェクトに、パラメータx, yを指定した試行(※)を加える関数を定義します。
(※)f(x, y)が最小になりそうなx, yの値を指定して、f(x, y)を求めること。

# Define utility functions for optimizing function efficiently.
def addPreliminaryTrials(study, base_params, choices):
    """Studyオブジェクトに、パラメータx, yを指定した試行を加える関数を定義する。
    ここのソースコードはちょっと難しいので、先に下の使用例を見るのがおすすめ。"""
    updated_params = base_params.copy()
    
    # Create all combinations of given parameters.
    value_combinations = itertools.product(*list(choices.values()))
    
    # Add fixed trials for all combinations of parameters to given study object.
    for value_combination in value_combinations:
        # Update given parameters.
        for key, value in zip(choices.keys(), value_combination):
            updated_params[key] = value
        
        # Add a fixed trial.
        study.enqueue_trial(params=updated_params, skip_if_exists=True)
        
    return study

Studyオブジェクトに、パラメータx, yを指定した試行(= x, yを指定して、f(x, y)を求めること)を加えます。ここでは、事前に、-5 < x, y < 5の範囲で、最小になることが分かっているものとします。その前提の元で、以下の、該当の範囲での目的関数の分布を確認するための、16個の試行を加えます。
(x, y) = (-5, -5), (-5, -2), (-5, 2), (-5, 5), (-2, -5), (-2, -2), (-2, 2), (-2, 5), (2, -5), (2, -2), (2, 2), (2, 5), (5, -5), (5, -2), (5, 2), (5, 5),

# Add few preliminary fixed trials for optimizing function efficiently.
base_params = {
    "x": 1,
    "y": 1,
}
choices = {
    "x": [-5, -2, 2, 5],
    "y": [-5, -2, 2, 5],
}
study = addPreliminaryTrials(study=study, base_params=base_params, choices=choices)
study.trials

加えた試行の回数だけ、最適化を行います。

# Run preliminary fixed trials.
n_trials = len(study.trials) # number of preliminary fixed trials
study.optimize(objective, n_trials=n_trials)

すると、以下のような結果が得られます。グラフ上の一点一点が一回の試行の結果を表しています。まだ、試行の回数が少ないので、最適化された値は、最小値(= 0)とは、ほど遠い値になっています。が、どうやら-2 < x, y < 2の範囲で、最小になりそだねー、ということが分かります。
#001_history.png
#001_contour.png

#002 決めた回数だけ最適化する。

最適化を進めるため、10回だけ試行を行います。ここからは、上記とは異なり、Optunaに、試行を行うパラメータx, yの値を決めてもらいます。上記と同じStudyオブジェクトを使えば、Optunaが、これまでの最適化の履歴を踏まえた、x, yの値を勝手に決めてくれます。試行の回数は、n_trialsで指定することができます。

# Run additional 10 trials. We can continue optimization just by calling optimize() using same study object. 
n_trials = 10
study.optimize(objective, n_trials=n_trials)

すると、以下のような結果が得られます。グラフ上の一点一点が一回の試行の結果を表しています。上記の結果と比べると、グッと最小値(= 0)に近づいていることが分かります。
#002_history.png
#002_contour.png

#003 決めた時間内に試せる回数だけ最適化する。

さらに、最適化を進めるため、回数を指定せずに、1秒間だけ試行を行います。ここでも、上記と同様、Optunaに、試行を行うパラメータx, yの値を決めてもらいます。これまでと同じStudyオブジェクトを使えば、Optunaが、これまでの最適化の履歴を踏まえた、x, yの値を勝手に決めてくれます。時間は、timeoutで指定することができます。Kaggleなどなど、計算機を使える時間が限られている場合は、この機能を使うのがおすすめです。

# Run additional trials in 1 seconds. We can continue optimization just by calling optimize() using same study object. 
timeout = 1.0
study.optimize(objective, timeout=timeout)

すると、以下のような結果が得られます。グラフ上の一点一点が一回の試行の結果を表しています。これまでの結果と比べると、グッと最小値(= 0)に近づいていることが分かります。
#003_history.png
#003_contour.png

#004 最適化した結果を表示する。

これまでに最適化した結果を表示します。目的関数の値とそれに対応したパラメータの値を表示することができます。

# Show optimization result. The results of previous trials is also included in the figure.
print(f"  Optimized value : {study.best_value}")
print(f"  Parameters : {study.best_params}")
  Optimized value : 0.020000000000000035
  Parameters : {'x': 0.10000000000000053, 'y': -0.09999999999999964}

#005 最適化の履歴をプロットする。

これまでの最適化の履歴をプロットします。グラフ上の一点一点が一回の試行に対応しています。

# Plot optimization history. The results of previous trials is also included in the figure.
fig = optuna.visualization.plot_optimization_history(study)
fig.show()

#005_history.png

#006 目的関数の分布をプロットする。

これまでに最適化した結果に基づいて推定される、目的関数の分布(等高線)をプロットします。グラフ上の一点一点が一回の試行に対応しています。パラメータをどの程度の値にしたら良いのか、パッと見で分かるので、ものすごく便利な機能です。

# Plot contour of parameters. The results of previous trials is also included in the figure.
fig = optuna.visualization.plot_contour(study, params=["x", "y"])
fig.show()

#006_contour.png

#007 パラメータの重要度をプロットする。

最適化するのに、どのパラメータをいじるのが良いのか、その程度を示す指標(= 重要度)をプロットします。

# Plot parameters importances. The importances of x and y are expected to be same value for the example,
# but those are different because of limited number of trials. Those will be closer if addtional trials are run.
fig = optuna.visualization.plot_param_importances(study)
fig.show()

#007_importance.png

#008 目的関数とパラメータの値の関係をプロットする。

目的関数とパラメータの値の関係をプロットします。パラメータをどの程度の値にしたら良いのか、パッと見で分かるので、ものすごく便利な機能です。見たいパラメータの数が二つ以上の場合は、等高線が描けないので、こちらを使うしかありません。

# Plot parameters relation.
fig = optuna.visualization.plot_parallel_coordinate(study, params=["x", "y"])
fig.show()

#008_relation.png

#009 最適化の履歴を保存する。

これまでの最適化の履歴をファイルに保存するための関数を定義します。Optunaのチュートリアルでは、RDB(データベース)に保存することが推奨されていますが、ここでは、RDBを用意する手間を省くため、Pickleファイルに保存するようにしています。Kagglerには、こちらの方が、手軽に扱えるので良いかなと思います。

def toPickle(obj, path_to_pickle):
    """objをPickleファイルとして保存する。"""
    with open(path_to_pickle, "wb") as fout:
        pickle.dump(obj, fout)

これまでの最適化の履歴をファイルに保存します。

# Save optimization history (study object) as pickle file.
path_to_study = "/kaggle/working/study.pkl"
toPickle(obj=study, path_to_pickle=path_to_study)
!ls {path_to_study}

#010 保存した最適化の履歴から、再び最適化を始める。

保存した最適化の履歴を読み込むための関数を定義します。

def fromPickle(path_to_pickle):
    """Load obj from pickle file."""
    with open(path_to_pickle, "rb") as fin:
        return pickle.load(fin)

保存した最適化の履歴を読み込んで、そこから最適化を開始します。もはや、これほどの回数の試行を行う意味はないのかも知れませんが、1000回試行を行います。

# Reload saved study object and restart optimization (Run additional 1000 trials. It is
# meaningless calculation, just an example for showing how to restart optimization.).
study_from_pickle = fromPickle(path_to_pickle=path_to_study)
n_trials = 1000
study_from_pickle.optimize(objective, n_trials=n_trials)

すると、以下のような結果が得られます。

  Optimized value : 0.0
  Parameters : {'x': 0.0, 'y': 0.0}

#010_history.png
#010_contour.png

4. 最後に

生き物は必ず死を迎えます。それも、いつ訪れるのかわかりません。明日ミサイルが飛んでくるかも。時間は有限です。その中で、如何にやりたいことを成し遂げるのか、その鍵は時間の使い方にあります。計算機にやれる仕事はすべて、計算機にやってもらいましょう。そして、自分にしかできないこと、やりたいことをやりましょう。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5