Optuna vs Hyperopt - 次元の呪いへの耐性比較

:::note warn
次元の呪いは、単に次数が高いだけではなく、データが複雑であることが重要（相関が明確で重要な特徴量が限定される場合は大して呪われない気がする）なのだと思いますが、この検証ではデータ生成時に乱数に頼ってる部分があるため、結果はあまり参考にならないかもしれません。
:::
## 1. 次元の呪いとは
次元の呪い（Curse of Dimensionality）とは、機械学習や最適化問題で特徴量（次元）の数が増えることで、モデルの学習や最適化が難しくなる現象のこと。特徴量の次元が増えることで、探索空間が指数的に広がり、最適な解を見つけるために必要な計算リソースや時間が増加します。

## 2. 登場するチューナー紹介
ハイパーパラメータのチューニングツールといえば、**Optuna**と**hyperopt**！
ということで、この2チューナーの次元の呪いへの耐性を比較します

### Optuna
Optunaは、Preferred Networksによって開発されている、効率的なハイパーパラメータチューニングを提供するライブラリで、特に **TPE(Tree-structured Parzen Estimator)** というベイズ最適化アルゴリズムが特徴です。Optunaは、ユーザーが簡単に学習プロセスを最適化できるよう、分かりやすく柔軟なAPIを提供しています。

### Hyperopt
Hyperoptは、ベイズ最適化をベースにしたハイパーパラメータチューニングのライブラリで、**TPE(Tree-structured Parzen Estimator)** や**random search**, **Annealing**などのアルゴリズムが利用できます。Optunaと似た機能を持ちながらも、やや設定や使い方が複雑な場合があるそうです。
私はあまり使ったことがありませんでした...

## 3. 実験
### 概要
次元の呪いの影響を確認するために、2つのアルゴリズムで次元数（特徴量数）が増えるときの最適化の速度と性能（損失値）を比較します。
コードは以下の通りですが、Hyperoptの使い方が正しいのかどうか、あまり自信がありません...
間違いなどありましたら、ぜひ教えてください。

**《やってること(やりたいこと)》**
1. **Optunaによる目的関数**  
   `optuna_objective`関数は、Optunaを使ってハイパーパラメータの最適化を行う
   `n_estimators`、`max_depth`、`min_samples_split`を調整し、`RandomForestClassifier`を訓練して精度を評価し、最終的に誤差（1 - 精度）を返す

2. **Hyperoptによる目的関数**  
   `hyperopt_objective`関数は、Hyperoptを使って最適化を行う
   Optunaと同様にモデルを訓練するが、ハイパーパラメータの空間定義に`hp.randint`を使い、最適化には`fmin`を使用している

3. **実験の実行と結果の記録**  
   `run_experiment`関数は、指定した特徴量の次元で実験を行う
   `tuner`でOptunaとHyperoptを順番に選び、最適化を行い、損失と処理時間を記録する。実験は特徴量の次元（10～250）を10ごとに加算して実施される


```python
import optuna
from hyperopt import hp, tpe, fmin, Trials
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import time
import matplotlib.pyplot as plt

def optuna_objective(trial, X_train, X_test, y_train, y_test):
    n_estimators = trial.suggest_int('n_estimators', 10, 200)
    max_depth = trial.suggest_int('max_depth', 2, 20)
    min_samples_split = trial.suggest_int('min_samples_split', 2, 10)
    model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, min_samples_split=min_samples_split, random_state=42)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    return 1 - accuracy

def hyperopt_objective(space, X_train, X_test, y_train, y_test):
    model = RandomForestClassifier(n_estimators=space['n_estimators'], max_depth=space['max_depth'], min_samples_split=space['min_samples_split'], random_state=42)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    return 1 - accuracy

def run_experiment(n_features, tuner='optuna'):
    X, y = make_classification(n_samples=1000, n_features=n_features, n_informative=n_features//2, n_classes=2, random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
    if tuner == 'optuna':
        study = optuna.create_study(direction='minimize')
        start_time = time.time()
        best_loss = float('inf')
        trials = 0
        no_improvement_count = 0
        while no_improvement_count < 100:
            study.optimize(lambda trial: optuna_objective(trial, X_train, X_test, y_train, y_test), n_trials=1)
            current_loss = study.best_value
            if current_loss < best_loss:
                best_loss = current_loss
                no_improvement_count = 0
            else:
                no_improvement_count += 1
            trials += 1
        end_time = time.time()
        total_time = end_time - start_time
    elif tuner == 'hyperopt':
        space = {'n_estimators': hp.randint('n_estimators', 10, 200), 'max_depth': hp.randint('max_depth', 2, 20), 'min_samples_split': hp.randint('min_samples_split', 2, 10)}
        trials = Trials()
        start_time = time.time()
        best_loss = float('inf')
        no_improvement_count = 0
        while no_improvement_count < 100:
            best = fmin(fn=lambda space: hyperopt_objective(space, X_train, X_test, y_train, y_test), space=space, algo=tpe.suggest, max_evals=1, trials=trials)
            current_loss = trials.best_trial['result']['loss']
            if current_loss < best_loss:
                best_loss = current_loss
                no_improvement_count = 0
            else:
                no_improvement_count += 1
        end_time = time.time()
        total_time = end_time - start_time
    return best_loss, total_time

feature_dims = list(range(10, 251, 10))
tuners = ['optuna', 'hyperopt']
results_loss = {'optuna': [], 'hyperopt': []}
results_time = {'optuna': [], 'hyperopt': []}
for dim in feature_dims:
    print(f"Start Experiment with {dim} features using Optuna")
    loss, time_taken = run_experiment(dim, tuner='optuna')
    results_loss['optuna'].append(loss)
    results_time['optuna'].append(time_taken)
    print(f"Complete Experiment with {dim} features using Optuna")
    
    print(f"Start Experiment with {dim} features using Hyperopt")
    loss, time_taken = run_experiment(dim, tuner='hyperopt')
    results_loss['hyperopt'].append(loss)
    results_time['hyperopt'].append(time_taken)
    print(f"Complete Experiment with {dim} features using Hyperopt")

plt.figure(figsize=(14, 6))

plt.subplot(1, 2, 1)
for tuner in tuners:
    plt.plot(feature_dims, results_loss[tuner], label=f'{tuner} Loss')
plt.xlabel('Feature Dimension')
plt.ylabel('Loss')
plt.title('Comparison of Loss vs Feature Dimension')
plt.legend()

plt.subplot(1, 2, 2)
for tuner in tuners:
    plt.plot(feature_dims, results_time[tuner], label=f'{tuner} Time')
plt.xlabel('Feature Dimension')
plt.ylabel('Time (seconds)')
plt.title('Comparison of Training Time vs Feature Dimension')
plt.legend()

plt.tight_layout()
plt.show()

```

### 結果
以下の通りです
#### 1回目
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748559/44d1e149-5ccc-87b8-70bb-06c7e8e1de49.png)

#### 2回目
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3748559/e6b2a04b-afa0-018c-4f89-e34c15dc3dde.png)

## 結論
期待通り、Optunaがlossの面では勝ってくれました
ただ、Hyperoptの速度がめちゃくちゃ速くて、驚くました。
これ、私のHyperoptの使い方が間違ってるからですかね...?
Hyperoptは使いこなせるようになれれば超高速で高性能なチューナーなのかも？、と思いましたが、今のところ使うつもりはないと思います
Optunaの使い勝手が好きで離れられそうにない...

---

ここまで読んでくださった方がいらっしゃいましたら、ありがとうございます。
間違いなどありましたら、ぜひ教えてください。

以上。