Optunaで学ぶベイズハイパーパラメータチューニング超入門
– 第5回: 複数の目的変数を持つチューニング –

Optunaで学ぶベイズハイパーパラメータチューニング超入門 – 第5回: 複数の目的変数を持つチューニング –

ハイパーパラメータチューニングは、機械学習モデルの性能を最大化するための重要なステップです。

前回は、チューニング時間の短縮に貢献するプルーニング」というお話しをしました。

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モデルの最適化テクニック –