機械学習を行う際に大事なのがパラメーターの調整です。
今まで適当にデフォルトの値でそのままやったりGridearchで探したりしていましたが、結構時間かかるので他の有効な方法を探して、手元で実際に動かして見ました。こちらの資料がわかりやすかったです。(図もこちらのものを引用しました)

Bayesian Optimization

Bayesian Optimizationはパラメーターをx、評価値(精度とか)をyとして

suushiki0

という関数を指定します(ブラックボックス関数)。中身は良くわかりませんが、この関数を最適化するパラメーターを見つけたいと思います。そこでBaysian Optimizationはこの関数がガウス過程に従うと仮定します。

下の図ではパラメーターの組み合わせをそれぞれ2,3個とって来て、その評価値を計算して結果をプロットしたグラフです。青い曲線はこの二点から導かれる関数の事後分布で、青い部分はこの分布の95%信頼区間です。

Baysian Optimization

このグラフを見ると、観測点から離れた部分は信頼区間の幅が広い(=σが大きい)ことがわかります。

獲得関数(Acquisition Function)

獲得関数は、次にどこの点を観測するか決める関数です。これにはいろいろな関数がありますが、よく使われるのが

suushiki2

というExpected Improvement[Mockus,1978]であったり、
suushiki1

のようなMutual Information[Contal+2014]がよく使われます。後者は特に直感的にわかりやすいと思うのですが、私たちも次にどの点を選ぶかというときに

  • 観測した点から推測して精度が良さそうな点を選びたい(μが大きい)
  • まだ観測していない場所から選びたい(σが大きい)

ということを考えて選びそうなものです。よくできてますね。

acquisition function

実装にはBayesian Optimizationを使いました。

使用するデータはkaggleのOtto Group Product Classification Challengeのデータで評価指標はmulti-class loglossです。定番のXGBoostのパラメータを最適化します。

pip install bayesian-optimization

import pandas as pd
import xgboost as xgb
from sklearn.preprocessing import LabelEncoder
from bayes_opt import BayesianOptimization

調整したいパラメーターを引数にとる評価関数の指定、クロスバリデーション。bayesian-optimizationには評価関数の最大化のライブラリしかないので、小さい値ほどいいloglossは返り値に-1をかけます。

def xgb_evaluate(min_child_weight,
colsample_bytree,
max_depth,
subsample,
gamma,
alpha):

params['min_child_weight'] = int(min_child_weight)
params['cosample_bytree'] = max(min(colsample_bytree, 1), 0)
params['max_depth'] = int(max_depth)
params['subsample'] = max(min(subsample, 1), 0)
params['gamma'] = max(gamma, 0)
params['alpha'] = max(alpha, 0)

cv_result = xgb.cv(params, xgtrain, num_boost_round=num_rounds, nfold=5,
seed=random_state,
callbacks=[xgb.callback.early_stop(50)])

return -cv_result['test-mlogloss-mean'].values[-1]

いよいよ最適化!

if __name__ == '__main__':
xgtrain = prepare_data()

num_rounds = 3000
random_state = 2016
num_iter = 25
init_points = 5
params = {
'eta': 0.1,
'silent': 1,
'eval_metric': 'mlogloss',
'verbose_eval': True,
'seed': random_state,
'num_class':9
}

xgbBO = BayesianOptimization(xgb_evaluate, {'min_child_weight': (1, 20),
'colsample_bytree': (0.1, 1),
'max_depth': (5, 15),
'subsample': (0.5, 1),
'gamma': (0, 10),
'alpha': (0, 10),
})

xgbBO.maximize(init_points=init_points, n_iter=num_iter)

結果です

result
result2

だいたい15回くらいの試行でloglossが0.46136まで下がりました。やってから気づいたんですが、max_depthとかって整数の値しかとらないですね、、、

ただし、ベイズ最適化には弱点もいくつかあって、

  • カテゴリー変数の場合にうまくいかない。
  • 偶然性に左右されたり、再現性が取れないことがある
  • バラメーターが増えてきたら時間かかる

みたいなことになるらしいです。

Tree-structured Parzen Estimator(TPE)

このような弱点を修正したのがTPEという最適化手法です。ベイズとコンセプトは似ていますが、手法は全く異なります。一般的な方法として、まずRandom Searchを用いていくつか点をとってきます。プロットすると下の図のようになりました。

TPE

次に精度が良かったもの(図では上位20%)とそうでなかったものに分けます。この2群の尤度関数を求めます。あまり尤度と言っても馴染みのない人が多いと思いますが、サンプリングされたデータは様々な確率分布のうち、どの分布から得られたものとするのが一番尤もらしいかを決めようとするものです。これにより2群の確率分布が出来上がります。

TPEでもExpected Improvement関数の下のように定義します。精度良かったものをl,そうではなかったものをgとして、

suushiki3
これをそれぞれの観測点に対して適用し、最もEIの値が大きかった場所が次の観測点になります。

prob dist
acq

こっちも実装してみます。Pythonではhyperoptというライブラリがあってpipで入ります。

pip install hyperopt

import hyperopt
from hyperopt import hp, tpe, Trials, fmin

最適化するパラメータはbayesian optimizationと同じやつにしてみました。

hyperopt_parameters = {'min_child_weight': hp.uniform('min_child_weight',1,20),
'colsample_bytree': hp.uniform('colsample_bytree',0.1, 1),
'max_depth': hp.choice('max_depth',np.arange(5, 15)),
'subsample': hp.uniform('subsample',0.5, 1),
'gamma': hp.uniform('gamma',0, 10),
'alpha': hp.uniform('alpha',0, 10),
}

最適化する関数の指定

def objective(args):
classifier = xgb.XGBClassifier(**args)
stratifiedkfold = StratifiedKFold(n_splits=5)
result = cross_val_score(classifier, train.drop(['id','target'],axis = 1), train.target, cv=stratifiedkfold,scoring='neg_log_loss')
return -result.mean()

実行!

max_evals = 50
trials = Trials() # 実行結果を格納するインスタンス

best = fmin(
objective,
hyperopt_parameters,
algo = tpe.suggest,
max_evals =max_evals,
trials = trials,
verbose = 1)

結果

loss

best_param

logloss最小値は0.4749でした。あれ、bayesian optimizaationより悪い、、、bayesianではmax_depthを整数に限定しなかったからかも、、
でも自分で手動でやった時は0.6とかだったんで、パラメーターチューニングの時にはこれからこれ使っていこうと思います。あとこのxgboost動かすのに8コアCPU使ってそれぞれ半日くらい回しました。GPU使ってたらもうちょい早かったと思うのですが、きちんとbuildとmakeしてもうまくいきませんでした。また挑戦します。