次元の呪いは、単に次数が高いだけではなく、データが複雑であることが重要(相関が明確で重要な特徴量が限定される場合は大して呪われない気がする)なのだと思いますが、この検証ではデータ生成時に乱数に頼ってる部分があるため、結果はあまり参考にならないかもしれません。
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の使い方が正しいのかどうか、あまり自信がありません...
間違いなどありましたら、ぜひ教えてください。
《やってること(やりたいこと)》
-
Optunaによる目的関数
optuna_objective
関数は、Optunaを使ってハイパーパラメータの最適化を行う
n_estimators
、max_depth
、min_samples_split
を調整し、RandomForestClassifier
を訓練して精度を評価し、最終的に誤差(1 - 精度)を返す -
Hyperoptによる目的関数
hyperopt_objective
関数は、Hyperoptを使って最適化を行う
Optunaと同様にモデルを訓練するが、ハイパーパラメータの空間定義にhp.randint
を使い、最適化にはfmin
を使用している -
実験の実行と結果の記録
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回目
2回目
結論
期待通り、Optunaがlossの面では勝ってくれました
ただ、Hyperoptの速度がめちゃくちゃ速くて、驚くました。
これ、私のHyperoptの使い方が間違ってるからですかね...?
Hyperoptは使いこなせるようになれれば超高速で高性能なチューナーなのかも?、と思いましたが、今のところ使うつもりはないと思います
Optunaの使い勝手が好きで離れられそうにない...
ここまで読んでくださった方がいらっしゃいましたら、ありがとうございます。
間違いなどありましたら、ぜひ教えてください。
以上。