4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

機械学習Advent Calendar 2024

Day 5

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

Last updated at Posted at 2024-12-04

次元の呪いは、単に次数が高いだけではなく、データが複雑であることが重要(相関が明確で重要な特徴量が限定される場合は大して呪われない気がする)なのだと思いますが、この検証ではデータ生成時に乱数に頼ってる部分があるため、結果はあまり参考にならないかもしれません。

1. 次元の呪いとは

次元の呪い(Curse of Dimensionality)とは、機械学習や最適化問題で特徴量(次元)の数が増えることで、モデルの学習や最適化が難しくなる現象のこと。特徴量の次元が増えることで、探索空間が指数的に広がり、最適な解を見つけるために必要な計算リソースや時間が増加します。

2. 登場するチューナー紹介

ハイパーパラメータのチューニングツールといえば、Optunahyperopt
ということで、この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_estimatorsmax_depthmin_samples_splitを調整し、RandomForestClassifierを訓練して精度を評価し、最終的に誤差(1 - 精度)を返す

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

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

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

2回目

a.png

結論

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


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

以上。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?