ハイパーパラメータチューニングは、機械学習モデルの性能を最大化するための重要なステップです。
前回は、「チューニング時間の短縮に貢献するプルーニング」というお話しをしました。
Optunaで学ぶベイズハイパーパラメータチューニング超入門 – 第4回: チューニング時間の短縮に貢献するプルーニング –
ハイパーパラメータチューニングを行う際、一般的には一つの目的変数を最適化します。例えば、機械学習モデルの訓練時には、精度を最大化するか、損失を最小化することが目標となることが多いです。
しかし、実際の問題設定では、複数の目的が重要な場面もあります。例えば、精度を最大化しつつ、モデルの計算量や推論速度も考慮したい場合などが挙げられます。
今回は、複数の目的変数を持つチューニングについてお話しします。
要は、多目的ベイズ最適化です。
マルチオブジェクティブチューニングとは?
マルチオブジェクティブチューニングは、複数の目的変数を同時に最適化するチューニング手法を指します。これにより、トレードオフの関係にある複数の目的を考慮しつつ、最適なハイパーパラメータを探索することができます。
Optunaは、マルチオブジェクティブチューニングをサポートしています。基本的な使用方法は、単一目的のチューニングと似ていますが、目的関数が複数返り値を持つ点が異なります。Optunaは、この複数の返り値を元にParetoフロントを計算し、最適なハイパーパラメータの組み合わせを探索します。
具体的な実装には、create_study
メソッドでdirections
引数を用いて、各目的の最大化・最小化の方向を指定します。そして、目的関数は複数のスカラー値をリストとして返すように設計します。
コード例
目的変数が1つの例と、2つの例を示します。
目的変数が1つの例
x^2+y^2が最小になるx \in [-10,10]、y \in [-10,10]を求めます。以下、コードです。
import optuna # 目的関数の定義 def objective(trial): # ハイパーパラメータのサンプリング x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) # 目的関数の計算 obj1 = x**2 + y**2 return obj1 # スタディの作成 study = optuna.create_study(direction='minimize') # 最適化の実行 study.optimize(objective, n_trials=100) # 結果の確認 print(study.best_value) print(study.best_params)
以下、実行結果です。
0.00022899005071326623 {'x': -0.013259050605635092, 'y': 0.007292984831361843}
目的変数が2つの例
目的変数が1つの例に対し、目的変数を1つ追加し、目的変数を2つにします。ちなみに、追加する目的変数は(x-5)^2+(y-5)^2です。
そのことで、目的変数を2つ持つになります。
- 1つ目の目的変数:原点からの距離を最小化
- 2つ目の目的変数:点(5, 5)からの距離を最小化
以下、コードです。
import optuna from optuna.multi_objective import create_study import math # 目的関数の定義 def objective(trial): # ハイパーパラメータのサンプリング x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) # 2つの目的関数の計算 obj1 = x**2 + y**2 obj2 = (x - 5)**2 + (y - 5)**2 return obj1, obj2 # マルチオブジェクティブのスタディを作成(2つの目的が最小化の場合) study = create_study(["minimize", "minimize"]) # スタディの最適化 study.optimize(objective, n_trials=100) # マルチオブジェクティブの結果(Paretoフロント)を表示 pareto_front_trials = study.get_pareto_front_trials() for trial in pareto_front_trials: print("Params {}: Values = {}".format(trial.params, trial.values))
以下の2つの目的変数を同時に最小化するx \in [-10,10]、y \in [-10,10]は存在しません。
- 1つ目の目的変数:原点からの距離を最小化
- 2つ目の目的変数:点(5, 5)からの距離を最小化
そのため、Paretoフロントに属する解とその目的変数の値を出力します。
以下、実行結果です。
Params {'x': 6.425931488841179, 'y': 4.042610251619363}: Values = (57.63529314577859, 2.9498757411731615) Params {'x': 1.4393072326258043, 'y': 2.705396888705655}: Values = (9.39077763530719, 17.943736421992597) Params {'x': 0.9062612381873247, 'y': 4.8717049536518715}: Values = (24.55481858727701, 16.775156668885046) Params {'x': 4.298397784458805, 'y': 3.142643489297182}: Values = (28.35243161426233, 3.942018876702461) Params {'x': 4.298397784458805, 'y': 3.142643489297182}: Values = (28.35243161426233, 3.942018876702461) Params {'x': 1.4848469575936427, 'y': -0.7328383836288861}: Values = (2.7418225839948955, 45.221736844347326) Params {'x': 0.6794412428159884, 'y': -0.41497878743627936}: Values = (0.6338477964614196, 47.98922324266433) Params {'x': 2.010972276805603, 'y': 1.1798850650477206}: Values = (5.4361382648033745, 23.52756484627014) Params {'x': 6.425931488841179, 'y': 4.042610251619363}: Values = (57.63529314577859, 2.9498757411731615)
Paretoフロントとは?
Paretoフロント(パレート解)は多目的(マルチオブジェクティブ)最適化の文脈で頻繁に使われる最適解の集合の概念です。
多目的最適化では、複数の目的関数を同時に最適化することを目指しますが、これらの目的関数は通常、互いにトレードオフの関係にあります。
つまり、一つの目的変数を改善することで、他の目的変数が悪化する可能性があります。
言い換えれば、Paretoフロントの解をさらに改善するには、少なくとも1つの他の目的変数を犠牲にしなければなりません。
例えば、車の設計における「燃費」と「加速性能」を考えると、これらは互いにトレードオフの関係にあります。
燃費を向上させるためには、車の重量を減らす、エンジンの出力を抑えるなどの対策が考えられますが、これにより加速性能が悪化する可能性があります。逆に、加速性能を向上させるためには、より大きなエンジンやターボを搭載するなどの対策が考えられますが、燃費が悪化する可能性があります。
このようなトレードオフの関係を持つ目的間での最適なバランスを見つけるために、Paretoフロントが使用されます。
解を1つに絞るにはどうすればいいのか?
多目的(マルチオブジェクティブ)最適化には、Paretoフロントと呼ばれる複数の解が存在します。困ったことにこれらの解は、トレードオフの関係にあります。
では、1つの解に絞るにはどうすればいいでしょうか?
最適な解を1つだけ選ぶためには、例えば以下のようなアプローチが考えられます。
- ビジネス要件やドメイン知識を考慮: ある目的変数が他の目的変数よりも重要であると判断される場合、その目的変数を重視して解を選択します。
- 重み付き和を使用: 各目的変数に重みを割り当て、重み付き和を計算します。この重み付き和が最小(または最大)となる解を選択します。
- 意思決定者との対話: エンドユーザーやステークホルダーとの対話を通じて、どの解が最も実用的かを判断します。
以下は、重み付き和を使用して最適な解を1つ選択する例です。
import optuna from optuna.multi_objective import create_study import math # 目的関数の定義 def objective(trial): # ハイパーパラメータのサンプリング x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) # 2つの目的関数の計算 obj1 = x**2 + y**2 obj2 = (x - 5)**2 + (y - 5)**2 return obj1, obj2 # マルチオブジェクティブのスタディを作成(2つの目的が最小化の場合) study = create_study(["minimize", "minimize"]) # スタディの最適化 study.optimize(objective, n_trials=100) # マルチオブジェクティブの結果(Paretoフロント)を取得 pareto_front_trials = study.get_pareto_front_trials() # 重み付き和を使用して最適なトライアルを選択 weights = [0.5, 0.5] best_trial = min(pareto_front_trials, key=lambda t: sum(w*v for w, v in zip(weights, t.values))) print("Best trial by weighted sum:") print(" Params: {}".format(best_trial.params)) print(" Values: {}".format(best_trial.values))
このコードでは、重みweights
を[0.5, 0.5]
としていますが、この重みは目的に応じて調整することができます。
以下、実行結果です。
Best trial by weighted sum: Params: {'x': 1.5490568061689896, 'y': 2.4228631024697993} Values: (8.269842602048051, 18.550643515660163)
まとめ
今回は、複数の目的変数を持つチューニングについてお話ししました。
目的変数を複数にすると、Paretoフロントという最適解の集合が登場します。複数の目的変数を同時に最適化する解が、通常は存在しないためです。
多くの場合、トレードオフの関係が生まれます。つまり、一つの目的を改善することで、他の目的が悪化する可能性があります。
実務では1つの解に絞る必要もあることでしょう。
- ビジネス要件やドメイン知識を考慮: ある目的変数が他の目的変数よりも重要であると判断される場合、その目的変数を重視して解を選択します。
- 重み付き和を使用: 各目的変数に重みを割り当て、重み付き和を計算します。この重み付き和が最小(または最大)となる解を選択します。
- 意思決定者との対話: エンドユーザーやステークホルダーとの対話を通じて、どの解が最も実用的かを判断します。
ちなみに、この多目的ベイズ最適化は、厳密な多目的最適化ではありませんので、その点だけ注意しましょう。
次回は、scikit-learnとOptunaを統合したOptunaSearchCV
を中心にお話しします。
Optunaで学ぶベイズハイパーパラメータチューニング超入門 – 第6回: OptunaSearchCVを活用したscikit-learnモデルの最適化テクニック –