Optunaで学ぶハイパーパラメータチューニング ― ベイズ最適化・TPE・多目的最適化まで

## はじめに

この前研究室のセミナーで **Optunaを使ったハイパーパラメータ最適化**について講義をしてきました。（僕の研究室には学部生から博士課程の方まで様々なので、わかりやすくてやりがいのある資料を作るのがなかなか難しかったです🤧）
そこで、せっかくなのでその内容をQiitaにまとめました。

本記事では
- **ハイパーパラメータとは何か？**
- **ハイパーパラメータ最適化の代表的アプローチ**（グリッドサーチ／ランダムサーチ／ベイズ最適化）
- **Optuna の内部で採用されている TPE（Tree-structured Parzen Estimator）の簡単な原理**
- **実装例**（単目的・多目的最適化，パレートフロントの可視化）

を理論とコード両面から解説します。

> **対象読者**: 機械学習初学者〜中級者で、ハイパーパラメータ最適化の背景をもう一歩深掘りしたい人。
## 目次
1. ハイパーパラメータとは？  
2. ブラックボックス最適化とは？  
3. ハイパーパラメータ最適化の手法  
4. ベイズ最適化の概要（一般的なGPベース）  
5. TPE: Optuna デフォルトサンプラー  
6. Optuna 実装例（単目的）  
7. 多目的最適化とパレート最適
8. その他Tips 
9. まとめ
10. 参考文献  

---

## 1. ハイパーパラメータとは？

| 種別 | 例                                                    | 変更のタイミング                 |
|------|-------------------------------------------------------|----------------------------------|
| **モデルパラメータ（θ）**   | ニューラルネットワークの重み・バイアス                 | 学習中に最適化（勾配降下など）     |
| **ハイパーパラメータ（λ）** | learning rate, batch size, ドロップアウト率, 木の深さなど | 学習**外**で設定・調整             |

ハイパーパラメータは *学習の方法論* や *モデル構造* を規定する外部設定値です。適切にチューニングしないとモデル性能が大きく変動します。
モデルパラメータとハイパーパラメータの違いは学習ループに含まれるか否かで判断できそうです。


---

## 2. ブラックボックス最適化とは？

> **定義**  
> あるシステムの入力 $x$ と出力 $y = f(x)$ は観測できるが、システム内部（関数形・勾配・メカニズム）は **不明** な状況で、$x$ を調整し評価関数（目的関数）を最小／最大化する手法。

式で表現すると：

$$
\min_{x\in\mathcal{X}} f(x),
\quad
f : \mathcal{X} \to \mathbb{R}
\quad (\text{解析形不明})
$$

- **利用可能な操作**：点 $x$ をクエリ → 評価値 $y=f(x)$ が返るのみ。  
- **勾配 $\nabla f$** やヤコビ行列などは取得不可。

---

## 3. ハイパーパラメータ最適化の手法

機械学習モデルの性能は、学習アルゴリズムそのものだけでなく、その挙動を制御する「ハイパーパラメータ」に大きく依存します。学習率や正則化の強さ、決定木の深さといったこれらの値を適切に設定するプロセスは、モデルの性能を最大限に引き出す上で不可欠です。

しかし、ハイパーパラメータの探索空間は広大であり、手動での調整や考えられる全組合せを試すことは、特に一度の評価に時間がかかるモデルでは現実的ではありません。この「ハイパーパラメータ最適化」という課題を効率的に解決するため、様々な自動探索手法が提案されてきました。

ここでは、その代表的なアプローチであるグリッドサーチ、ランダムサーチ、そしてベイズ最適化について、それぞれの特徴を以下で比較します。

| 手法            | 探索方法               | 特徴                          |
|-----------------|------------------------|-------------------------------|
| グリッドサーチ   | 全組合せ総当たり        | 実装容易・高再現性            |
| ランダムサーチ   | 一様ランダム            | 高次元でも有効                |
| ベイズ最適化     | サロゲート + 獲得関数   | 評価回数を削減                |

**グリッドサーチ**は、指定された候補値をすべて試すだけなので理解が容易で実装も簡単ですが、次元数が増えると組み合わせが爆発的に増加し、現実的な時間で完了しないという大きな欠点があります。（指数オーダー）
また離散値のみで連続値の探索が不可能です。

**ランダムサーチ**は、この問題を緩和するシンプルな代替案です。決められた回数の中で探索空間からランダムにハイパーパラメータをサンプリングすることで、効率的に領域を探ることができます。またグリッドサーチと異なり、連続値の探索も可能となります。ただしいずれにしても最適解を見逃す可能性があり、ランダム性があるため理論上は無限回の試行が必要となります。

一方、**ベイズ最適化**はさらに一歩進んだアプローチです。ブラックボックス最適化手法の1つ (SMBO)であり、過去の評価結果をもとに、次に試すべき最も有望な点を統計的に予測しながら探索を進めます。この「賢い」探索により、グリッドサーチやランダムサーチのような闇雲な試行を避け、特に評価コストが高い問題において、はるかに少ない試行回数で最適解に到達できるという強力な利点があります。（ただし実装は複雑になりがちです。)



![hyperparam_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111235/a2b5b8e6-7ef7-4432-bd48-aef095ee7197.png)
↑ベイズ最適化は効率良く最適解に辿り着いている様子が読み取れます。

---

## 4. ベイズ最適化の概要（一般的なGP ベース）

1. 高価な実評価関数$f$をサロゲートモデルとしてガウス過程で近似することを行い、$p(f \mid \mathcal{D}_n)$ を推定する。  
2. **獲得関数** $a(x)$ を最大化して次点 $x_{n+1}$ を選択。  
3. ブラックボックスを評価しデータ $\mathcal{D}$ を更新 → 1 に戻る。

### 4.1 ガウス過程の事後分布

```math
\displaystyle
\mu_n(x_*) = k_*^\top (K + \sigma^2 I)^{-1} \mathbf y, \
\sigma_n^2(x_*) = k(x_*,x_*) - k_*^\top (K + \sigma^2 I)^{-1} k_*.
```

### 4.2 Expected Improvement (EI)
EIの直感的意味は「現行最良値をどれだけ超えられるかの期待度」です。
以下のように定義されます。

$$
\operatorname{EI}(x) 
\triangleq (y^* - \mu(x)) \Phi\bigl(Z(x)\bigr) + \sigma(x)\~\varphi\bigl(Z(x)\bigr),
\quad
Z(x) = \frac{y^* - \mu(x)}{\sigma(x)},
$$

ここで  
- $y^*$: これまでに観測された最良値（最小化問題なら最小値）
- $\mu(x),;\sigma(x)$: 上記ガウス過程が予測する，新点 $x$ の平均と標準偏差
- $\Phi(\cdot)$: 標準正規分布の累積分布関数(CDF)
- $\varphi(\cdot)$: 標準正規分布の確率密度関数(PDF)


---  

この式は大きく 2 つの項に分かれます。  

- **第1項**  
  $$
  (y^* - \mu(x))\\Phi\bigl(Z(x)\bigr)
  $$  
  - 平均予測でどれだけ改善余地があるか：$(y^* - \mu(x))$  
  - その改善が実現する確率：$\Phi(Z(x))$  
  → **改善余地 × 実現確率**  

- **第2項**  
  $$ 
  \sigma(x)\varphi\bigl(Z(x)\bigr)
  $$  
  - 予測の不確かさ：$\sigma(x)$  
  - その不確かさが示す場所の有用性（密度）：$\varphi(Z(x))$  
  → **不確かさ × 密度**  

両項を足し合わせることで、  
- 平均的に良さそうな場所（第1項）  
- 未知ゆえに試してみる価値がある場所（第2項）  

のバランスを取った指標になります。  
ベイズ最適化では、この $\operatorname{EI}(x)$ を最大化する点を次の試行点として選びます。


---

## 5. TPE: Optuna デフォルトサンプラー

Optunaの探索点は デフォルトではどう決定されるかというと、先ほど紹介した一般的なGP-BOではなくTPE(tree-structured parzen estimator)を用いています。
GP-BOとTPEの比較は以下の通りです。

| ベイズ最適化手法            | モデル化の対象   |   確率モデル      | コスト             | 
|-----------------|------------------------|-------------------------------|-------- |
| TPE   | $p(x\mid y)$        | 離散・連続モデル両方に対応            |低 |
| GP-BO   | $p(y\mid x)$          | 連続モデルを基本的に期待               |高 |

TPEは一般的な事後分布$p(y\mid x)$  ではなく，変数の不確実性(ある評価値 $y$ が得られるときのハイパーパラメータ $x$ の確率分布) $p(x\mid y)$  をモデル化するのが特徴的と言えます。



### TPE (Tree-structured Parzen Estimator) の詳細

TPEは、過去の試行結果を「良い評価値のグループ」と「悪い評価値のグループ」に分け、次に探索すべき有望な点を効率的に見つけ出す手法です。

#### 記法

まず簡単に記法を整理します。

-   **$x$**: 最適化する$d$次元のハイパーパラメータ点。
-   
    ```math
    x \triangleq (x^1, \dots, x^d) \in \mathcal{X}\quad (x^i \in \mathbb{R})
    ```

    
-   **$\mathcal{H}_t$**: $t-1$番目までの試行が終了した時点での、探索点と評価値の履歴（ヒストリ）。
    ```math
    \mathcal{H}_t \triangleq  \{(x_i, y_i)\}_{i=1}^{t-1}
    ```

    
#### 確率モデル

TPEは「ある評価値 $y$ が得られるときのハイパーパラメータ $x$ の確率分布」をモデル化するしますが、具体的には評価値の閾値 $y^*$ を使って、以下のように2つの条件付き確率分布を仮定します。

```math
p(x|\mathcal{H}_t, y) \triangleq 
\begin{cases}
l(x) & (y < y^*) \\
g(x) & (y \ge y^*)
\end{cases}
```

-   **$l(x) \triangleq  p(x | y < y^*)$**: **良い評価値の確率分布**。
    これまでの履歴の中で、評価値が良かった点（$y < y^*$）の集合から推定される $x$ の分布です。
-   **$g(x) \triangleq  p(x | y \ge y^*)$**: **悪い評価値の確率分布**。
    評価値が悪かった点（$y \ge y^*$）の集合から推定される $x$ の分布です。

※これらの確率分布は、通常Parzen Window (カーネル密度推定, KDE) と呼ばれる手法でデータから直接推定されます。

#### 獲得関数と次の探索点

TPEの目的は、「良い評価値の分布 $l(x)$ では出現しやすく、悪い評価値の分布 $g(x)$ では出現しにくい」点を見つけることです。これを数式で表現したものが獲得関数 $a(x)$ です。

$$a(x) \propto \frac{l(x)}{g(x)}$$

獲得関数 $a(x)$は通常EI(期待改善量)を用いて定義され、式変形(ここでは省略)で$\frac{l(x)}{g(x)}$の比例形になることが導出可能です。
この獲得関数 $a(x)$ を最大化する点が、次に試すべき最も有望な点となります。

$$x_{next} = \underset{x}{\operatorname{argmax}} \~ a(x)$$

これにより、ヒストリ的に見て「良さそうなゾーン」を集中的に探索することができます。

#### 探索と活用のトレードオフ

TPEでは、良い・悪いを分ける閾値 **$y^*$ を変化させることで、「探索」と「活用」のバランスを調整**できます。

-   **活用 (Exploitation)**: $y^*$ を低く設定すると、ごく一握りの最良点のみが「良い」と見なされます。その結果、$l(x)$ は既知の最良点の周辺に強く集中し、その近くを深く探索する「活用」が促進されます。
-   **探索 (Exploration)**: $y^*$ を高く設定すると、より多くの点が「良い」と見なされ、$l(x)$ の分布が広がります。これにより、まだ試していない未知の領域にも探索範囲が広がり、「探索」が促進されます。


---

## 6. Optuna実装例（単目的最適化）

実際にOptunaを使って単目的最適化を行ってみます。最適化の実装は以下のステップに従います。
1. 目的関数を定義
2. trial.suggest_floatで(整数ならint)ハイパーパラメータの探索範囲を定義
3. studyオブジェクトの作成 
3. study.optimizeで最適化を実行

例えば $(x-2)^2$ の最小化について考えてみましょう。

```python
import optuna

def objective(trial):
    x = trial.suggest_float("x", -10, 10) # -10 から 10 の範囲で探索する
    return (x - 2) ** 2
    
# 最小化する目的関数を指定して最適化を行う
study = optuna.create_study()
study.optimize(objective, n_trials=100)
```

これで、目的関数を最小化する $x$ の値が求まります。最適化の結果は以下のように確認できます。
```python
best_params = study.best_params
found_x = best_params["x"]
print("Found x: {}, (x - 2)^2: {}".format(found_x, (found_x - 2) ** 2))
```

```markdown
Found x: 2.0080527707866604, (x - 2)^2: 6.48471173424919e-05
```
ちゃんと2近くをとっていることがわかりますね。

可視化も簡単に行うことができます。

```python
import optuna.visualization as vis

# 最適化の過程を可視化
vis.plot_optimization_history(study)
```
![newplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111235/1342d32c-2493-432a-9934-5d8ca3f3b99a.png)

---
## 7. 多目的最適化とパレート最適

ここまでの例は、目的関数は1つだけでしたが、Optunaでは多目的最適化も可能です。
多目的最適化ではパレート最適(Pareto Optimal)という他の候補点を支配する点(組み合わせ)が存在することがあります。またパレート最適な点の集合をパレートフロントと呼びます。


### 7.1 （補足）パレート最適の定義

ここで「点 $x^*$ がパレート最適(Pareto Optimal)」というのは、以下のように表現できます。

```math
\nexists\,x\;\bigl(f_j(x)\le f_j(x^*)\;\forall j\bigr)
\;\land\;
\exists\,k\;\bigl(f_k(x)<f_k(x^*)\bigr).
```

### 7.2 ケース①

実際に以下の2つの関数の最小化を求める多目的最適化を行ってみます。

$$
f_1(x,y) = 4x^2 + 4y^2,
\quad
f_2(x,y) = (x-5)^2 + (y-5)^2
$$


```python
def f1(x, y):
    return 4 * x ** 2 + 4 * y ** 2

def f2(x, y):
    return (x - 5) ** 2 + (y - 5) ** 2

def objective(trial):
    x = trial.suggest_float('x', -10, 10)
    y = trial.suggest_float('y', -10, 10)
    return f1(x, y), f2(x, y)

study = optuna.create_study(directions=['minimize', 'minimize'])
study.optimize(objective, n_trials=1000)
```

パレートフロントを可視化することも可能です。

```python
# パレートフロントを可視化

# all trials
optuna.visualization.plot_pareto_front(
  study,
  include_dominated_trials=True
  ).show()

# best trials
optuna.visualization.plot_pareto_front(
  study,
  include_dominated_trials=False
  ).show()
```
![newplot-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111235/91e0e4a8-ebe0-4c08-9b24-451837487196.png)

解析的には両方下に凸の2変数関数なので、加重和をとって$x,y$それぞれで偏微分することでパレートフロントが求められますね。（対称性より$y=x$が設定変数空間でのパレートフロントだとすぐわかりますが😖）

### 7.3 ケース② — 制約条件付き

ではこんな関数での多目的最適化はどうなるでしょうか？
一度、設計変数空間でのパレートフロントを予想してからOptunaで確認してみると面白いかもしれません。


$$
\begin{aligned}
  &\min_{(x,y)\in\mathbb{R}^2}\bigl(f_1(x,y),\~f_2(x,y)\bigr)\\
  &\text{s.t.}\quad \frac{x^2}{4} + y^2 = 1
\end{aligned}
$$


$$
\begin{align}
f_1(x,y)= \left(\frac{x^2}{4} - y^2\right)^2，f_2(x,y) = x^2y^2
\end{align}
$$

```python
import math
import numpy as np
import matplotlib.pyplot as plt
import optuna

# ─────────────────────────────────────────
# 1. 目的関数の定義
#    - Optuna の Trial オブジェクトを受け取り
#      パラメータをサンプリング
#    - 2 つの目的関数 f1, f2 を計算して返却
# ─────────────────────────────────────────
def objective(trial: optuna.trial.Trial):
    # 制約条件で発生するxに関する定義域[-2, 2] からサンプリング
    x = trial.suggest_float("x", -2.0, 2.0)
    # 符号選択
    sign = trial.suggest_categorical("sign", [1, -1])
    # sqrt の対象が負になる点は無効なので安全に小さなクリップを行う
    inside = max(0.0, 1.0 - (x**2) / 4.0)
    y = sign * math.sqrt(inside)

    # 2 つの関数を同時に最小化する多目的最適化問題を考える

    # 穴埋め
    f1 = (x**2/4 - y**2) ** 2
    f2 = (x * y) ** 2

    # 後で決定変数をプロットするために user_attr に保存
    trial.set_user_attr("x", x)
    trial.set_user_attr("y", y)

    # Optuna はタプルで複数目的を扱う
    return f1, f2


# ─────────────────────────────────────────
# 2. Optuna による最適化の実行
#    - directions: 最適化方向を並べる
#    - n_trials: 試行回数を 500 に設定
# ─────────────────────────────────────────

# 穴埋め
study = optuna.create_study(
    directions=["minimize", "minimize"],
    sampler=optuna.samplers.TPESampler(multivariate=True),
)
study.optimize(objective, n_trials=500, show_progress_bar=True)
```
設定変数空間上のパレートフロントを可視化してみます。
```python
# ─────────────────────────────────────────
# 3. 結果のパレート判定とプロット
#    - 完了したトライアルを抽出
#    - 各試行の user_attr から (x,y) を取得
#    - 目的関数値 vals と組み合わせてパレート判定
#    - 設計変数空間に Pareto と non-Pareto をプロット
# ─────────────────────────────────────────
# 完了したトライアルのみをリストに集約
trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
# 各試行の (x, y) 座標を numpy 配列に格納
coords = np.array([[t.user_attrs["x"], t.user_attrs["y"]] for t in trials])
# 各試行の目的関数値を numpy 配列に格納
vals = np.array([t.values for t in trials])

# パレート最適判定関数を定義
def is_dominated(i):
    v = vals[i]
    #他の点で両方 <= かつ少なくとも一方 < の場合は支配される
    return np.any(np.all(vals <= v, axis=1) & np.any(vals < v, axis=1))

# 各試行がパレート最適かどうかのマスクを作成
pareto_mask = np.array([not is_dominated(i) for i in range(len(trials))])
nonpareto_mask = ~pareto_mask

# プロットの準備
fig, ax = plt.subplots(figsize=(6, 6))
# 非パレート点をプロット(薄いグレー)
ax.scatter(
    coords[nonpareto_mask, 0], coords[nonpareto_mask, 1],
    s=30, facecolors='none', edgecolors='gray', label='Non-Pareto'
)
# パレート最適点をプロット(濃い赤)
ax.scatter(
    coords[pareto_mask, 0], coords[pareto_mask, 1],
    s=30, color='C3', label='Pareto optimal'
)

x_line = np.linspace(-2, 2, 400)
# 制約条件: x^2/4 + y^2 = 1
y_pos = np.sqrt(np.clip(1 - (x_line**2) / 4.0, 0, None))
y_neg = -y_pos
ax.plot(x_line, y_pos, 'k--', lw=1, label='Constraint')
ax.plot(x_line, y_neg, 'k--', lw=1)

# 描画設定
ax.set_aspect('equal', 'box')
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-1.2, 1.2)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Pareto vs Non-Pareto')
ax.legend()
ax.grid(True)
plt.tight_layout()
plt.show()
```
![Unknown-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4111235/c9714d09-ab95-496b-92c9-610289e62b33.png)

楕円上でサンプリングされた全点がパレート最適になりました。

これは偏微分をしなくても、高校数学のように$\theta$を用いて媒介変数表示をすると気づくかもしれません。
パラメータ化：

$$
(x,y) = \bigl(2\cos\theta,\sin\theta\bigr)
\quad\Longrightarrow\quad
f_1 + f_2 = \cos^2(2\theta) + \sin^2(2\theta) = 1
$$

→ $f_1, f_2$は完全なトレードオフになり、**楕円上の全点がパレート最適**

---

## 8. その他Tips

- `TPESampler(gamma=0.4)` で探索寄りに  
- `MedianPruner` で早期終了  
- `study.optimize(..., timeout=3600)` で時間制限 
 
https://github.com/optuna/optuna

---
## 9. まとめ
今回ハイパーパラメータチューニングについて、Optunaの使い方からその数理的な背景（ベイズ最適化、GO-BP、TPE）まで少し踏み込んだ解説をしてみました。
また多目的最適化についてはパレートフロントを考えながら実装することで少し楽しい演習ができたかも（？）
個人的にTPEはそこまで詳しくなかったため勉強になりました。（ラボのセミナー担当してよかった！）

---
## 10. 参考文献

[1] Bergstra et al., *Algorithms for Hyper-Parameter Optimization* (NIPS 2011)  
[2]Optuna Docs: <https://optuna.org>