グラフ機械学習と強化学習について

主にグラフ機械学習や強化学習手法を記載します。

ベイズ最適化 (その3):ハイパーパラメータ探索

前回の続きです。

udnp.hatenablog.com

目次

ベイズ最適化が良く用いられる例だと思います。他にはハイパーパラメータの探索にはグリッドサーチや最適化計算を行う方法もあります。ハイパーパラメータの束縛条件を設定し、とりあえず試すだけなので簡単です。

xgboostのハイパーパラメータを下記手法

  • Randomized Search
  • Grid Search
  • Bayesian Optimization
  • Nevergrad

で探索し、結果を比較してみようと思います。データセットはsklearn.datasetsにあるdiabetesです。結論から言うとPSOが最も性能が良かったです。場合によるのでしょうが。

全体のソースコードは以下です。

github.com

baseline

まずは探索無しのデフォルトの場合です。

from sklearn import datasets
from sklearn.model _selection import GridSearchCV, cross_val_score
from sklearn.preprocessing import StandardScaler
from xgboost import XGBRegressor 
import numpy as np 
import matplotlib.pyplot as plt

X,y = datasets.load_diabetes(return_X_y=True)

scaler = StandardScaler()
xgb = XGBRegressor()
pipe = Pipeline([('scaler': scaler), ('xgb', xgb)])
 
scoring = 'neg_mean_squared_error'
baseline = cross_val_score(pipe, X, y, scoring=score, cv=5)

必要なパッケージを呼び出して、パイプラインに通します。前処理は特に必要ないのですが、正規化しています。 デフォルトの結果を示します。

print(baseline.mean())
> -3435.44

Algorithm

Document

適当に探索する方法です。

param = { 'xgb__gamma' range(0, 5), 'xgb__max_depth': range(1, 50), 'xgb__min_child_weight': range(1, 10), 'xgb__n_estimators' : range(0, 300)
rs = RandomizedSearchCV(pipeline, param_distributions=param, scoring=scoring, n_iter=25)

Document

ハイパーパラメータをグリッド上にして網羅的に計算しますが、ハイパーパラメータが多いと時間がかかるのとドメイン知識が必要となります。

param = { 'xgb__gamma': [0.01, 0.1] , 'xgb__max_depth': [1, 3, 5],  'xgb__min_child_weight': [1, 3, 10],  'xgb__n_estimators' : [50, 100, 500]}
gs = GridSearchCV(pipe, param_distributions=param, cv=5, scoring=scoring, iid=False)

GPyOpt

ベイズ最適化を用いてハイパーパラメータを探索します。GPでは、自作kernelも渡すことができます。獲得関数も複数から選択できます。 目的関数ですが、交差検証でのスコア値が最大になるようにベイズ最適化を行っていきます。

import GPy
import GPyOpt
from GPyOpt.methods import BayesianOptimization

bounds = [
    {'name': 'learning_rate', 'type': 'continuous', 'domain': (0, 0.1)}, 
    {'name': 'gamma', 'type': 'continuous', 'domain': (0, 5)}, 
    {'name': 'max_depth', 'type': 'discrete', 'domain': (1, 50)}, 
    {'name': 'n_estimators', 'type': 'discrete', 'domain': (1, 500)}, 
    {'name': 'min_child_weight', 'type': 'discrete', 'domain': (1, 10)}, 
]

def cv_score(*args):
    args = args[0]
    score = cross_val_score(
        XGBRegressor(learning_rate=args[0],
                                gamma=args[1],
                                max_depth = int(args[2]),
                                n_estimators=int(args[3]),
                                min_child_weight=int(args[4)),
        X, y, scoring=score, cv=5).mean()
        return score        

optimizer = BayesianOptimization(f=cv_score, domain=bounds, model_type='GP', acquisition_type='EI')
optimizer.run_optimization(max_iter=20)

Nevergrad

勾配を必要としない最適化アルゴリズムが多数実行できるようなパッケージになります。現時点で使えるアルゴリムは 100個ほどあり下記で確認できます。

from nevergrad.optimization import registry
print(sorted(registry.keys()))

BOも入っているようで、nervergradでもベイズ最適化できます。githubを見るとTwoPointsDEが良いみたいです。worker数の選択が大事になりそうです。まずはnevergrad用に目的関数を設計します。

def cv_score2(*args):
    lr, gamma, max_depth, n_est, min_child = args
    score = cross_val_score(
        XGBRegressor(learning_rate=lr[0],
                                gamma=gamma[0],
                                max_depth = int(max_depth),
                                n_estimators=int(n_est),
                                min_child_weight=int(min_child)),
        X, y, scoring=score, cv=5).mean()
        return -score                        

各ハイパーパラメータごとにサンプリング分布の設定や境界条件などを設定し最適化計算を行います。

from nevergrad.optimization import optimizerlib
from nevergrad import instrumentation as inst

# log distributed between 0.001 and 1
lr = inst.var.Array(1).bounded(0, 3).exponentiated(base=10, coeff=-1)
gamma = inst.var.Array(1).bounded(0, 3)
max_depth = inst.var.OrderedDiscrete(range(1, 50))
n_estimators = inst.var.OrderedDiscrete(range(1, 500))
min_child = inst.var.OrderedDisrete(range(1, 10))

instrumentation = inst.Instrumentation(lr, gamma, max_depth, n_estimators, min_child, num_workers=5)
optimizer = optimizerlib.TwoPointsDE(instrumentation, budget = 100)
args, kwargs = instrumentation.data_to_argument([0] * instrumentation.dimension)

for _ in range(optimizer.budget):
    x = optimizer.ask()
    y = cv_score2(*x.args)
    optimizer.tell(x, y)

recommendation = optimizer.provide_recommendation()
print(recommendation.args, cv_score2(*recommendation.args))

budgetがイテレーション数です。askで計算をし、tellで結果を返し、stepを更新していきます。

色々な最適化計算ができるのですが、ほかの手法を使うときは

optimizer = optimizerlib.PSO(instrumentation, budget = 100)

となります。

結果

baselineではneg MSEは -3435.44です。CV=5, イテレーションは30です。Grid searchは比較できないですが参考に載せています。

Algorithm time (sec) neg-MSE
Random 47.98 -3241.93
Grid 74.47 -3193.28
BO 65.08 -3218.64
RS 52.64 -3284.53
2ptsDE 47.62 -3338.47
FastGA1+1 35.08 -3196.43
PSO 56.65 -3161.58

基本的にベースラインは超えるので、ハイパーパラメータサーチは行った方がいいですが、手法間でそこまで大差があるとは思えませんね。

所感

多数手法があるので適当にアルゴリズムを選択しましたが、PSOが最もよかったです。最適化計算は大体PSO使ったときが性能が高い気がします。それにしても物凄く簡単にハイパーパラメータ探索ができました。分子生成の最適化計算時にも試してみたいです。

最適化計算の問題は、やはり計算量が膨大となることですね。TorqueなどMPIは必須でしょう。 アニーラーやゲート型コンピューターが発展してきたとき、どうなるか楽しみです。

引用・参考元

ベイズ最適化のところは、主に下記のブログから引用しています。

krasserm.github.io

github.com