Optunaは必ずしも「まんべんなく」試行してくれるわけではない件
今日、ちょっとハマったのでシェア。
はじめてのOptuna体験をしました
実は筆者は本日、自力では生まれてはじめて、Optunaを使いました。以下の、いかにもありそうなサンプルを実行して理解し、「あー、試行錯誤を助けてくれるソフトなぁんだな」と理解しました。
#------------------------------------------------------------------------------
# プログラム01: 領域[-10, 10]から、(x - 5)の2乗を最小にする値xを探すプログラム
#------------------------------------------------------------------------------
!pip install optuna
import optuna
# 目的関数の定義
def objective(trial):
# 目的関数の中で、お試しする変数、お試し範囲、ばらつかせ方を定義する
x = trial.suggest_uniform('x', -10, 10) # uniformって「一様」だよね・・・
# 目的関数の定義、この例だとx = 5 の時に最小(最適)となる
score = (x - 5) ** 2
return score
study = optuna.create_study(direction='minimize')
# 100回の試行錯誤を行う
study.optimize(objective, n_trials=100)
その後、以下を実行すると、最適だった時の値と、その時の試行値がわかります。
# 目的に対してベストだった値とを出力する
print(f'Best value: {study.best_value}')
print(f'Best param: {study.best_params}')
筆者の実行結果の例
Best value: 0.00016056866662173057
Best param: {'x': 4.987328430775088}
まんべんなく試行してくれてる?
ところが・・・その試行状況は、私の想定とは少し異なるものでした。
x = trial.suggest_uniform('x', -10, 10)のように「uniform」という名前の関数を使っているものですから、てっきり指定した区間を一様に探索している、つまり「まんべんなく試行してくれている」と期待していたのですが・・・ヒストグラムを表示してみてビックリ。
import matplotlib.pyplot as plt
# トライアル結果からxの値を取得
x_values = [trial.params['x'] for trial in study.trials]
# xの値をヒストグラムにプロット
plt.figure(figsize=(10, 6))
plt.hist(x_values, bins=20, edgecolor='k', alpha=0.7)
plt.title('Histogram of x values from Optuna Trials')
plt.xlabel('x value')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()
ん?なんだろうこの正規分布風のゆがんだ分布は・・・
まんべんなく試行してて欲しかったのですが・・・
ちなみにですが、uniformという関数は通常、一様分布を意味します。例えば以下のプログラムで一様乱数を生成してヒストグラムにすると以下のようになります。
#------------------------------------------------------------------------------
# プログラム02: 領域[-10, 10]の間で一葉乱数を1000個生成しヒストグラムを作成
#------------------------------------------------------------------------------
import numpy as np
import matplotlib.pyplot as plt
# 一様分布に従う乱数を生成
data = np.random.uniform(low=-10, high=10, size=1000)
# ヒストグラムをプロット
plt.figure(figsize=(10, 6))
plt.hist(data, bins=20, edgecolor='k', alpha=0.7)
plt.title('Histogram of Uniform Distribution')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()
ですので、余計ビックリでした。GPT-4oのChatGPTにも聞いてみたのですが
などと押し問答となり、所望の回答は得られませんでした。
Optunaの試行は「Sampler」が決める
その後、いろいろと調べてようやく、Samplerというものが存在すること、および最適化のための試行はSamplerが決めていることに気づきました。
Efficient Optimization Algorithms — Optuna 3.6.1 documentation
これによると、デフォルトではTPESamplerというサンプラーが使われていて、最適と思われる値付近を重点的にサンプルするようにできているので、先ほどのようなヒストグラムになっていた、と気づきました。
そこで、最もランダムにサンプルしてくれそうなRandomSamperを用いて再テストしました。
#------------------------------------------------------------------------------
# プログラム03: 領域[-10, 10]から、(x - 5)の2乗を最小にする値xを探すプログラム
# x はランダムサンプリング
#------------------------------------------------------------------------------
!pip install optuna
import optuna
from optuna.samplers import RandomSampler # Import the RandomSampler class
# 目的関数の定義
def objective(trial):
# 目的関数の中で、お試しする変数、お試し範囲、ばらつかせ方を定義する
x = trial.suggest_float('x', -10, 10)
# 目的関数の定義、この例だとx = 5 の時に最小(最適)となる
score = (x - 5) ** 2
return score
study = optuna.create_study(direction='minimize', sampler=RandomSampler()) # この例では、一様分布に沿って x を提案してくれる
# 100回の試行錯誤を行う
study.optimize(objective, n_trials=100)
この100回の試行における x の値の振り方をヒストグラムにすると次のようになっていました。
これでようやく、「確実にまんべんなく探索させる」方法がわかりました。