個人的にはHyperoptが一番使いやすく感じましたが、GPyOptは以前scikit-learnで試したベイス最適化に近いアルゴリズムだったのでもう少し理解を深めてみようかと。
まだ手探り段階なので、使い方に関しては後から追記するかもしれません。
使い方:
ハイパーパラメータの設定:
まずはMNISTモデルにおけるハイパーパラメータの設定からです。今回はやや少なめで。
最適化するハイパーパラメータ:
・各層のドロップアウト率:連続値
・Dense層出力ユニット数:離散値
・validation_splitの比率:連続値
GPyOptではハイパーパラメータを以下のようなフォーマットで書きます。
params = [ {'name': 'Dropout_0', 'type': 'continuous', 'domain': (0.0, 0.5)}, {'name': 'Dropout_1', 'type': 'continuous', 'domain': (0.0, 0.5)}, {'name': 'Dropout_2', 'type': 'continuous', 'domain': (0.0, 0.5)}, {'name': 'Dropout_3', 'type': 'continuous', 'domain': (0.0, 0.5)}, {'name': 'Dense_0', 'type': 'discrete', 'domain': (128, 256, 512)}, {'name': 'Dense_1', 'type': 'discrete', 'domain': (64,128, 256)}, {'name': 'validation_split', 'type': 'continuous', 'domain': (0.1, 0.3)} ]
リスト化されたディクショナリーで(ここを参考に)、
・'name' : パラメータ名
・'type' : 'continuout'(連続値)、'discrete'(離散値)、'categorical'(分類値)
・'domain' : 適用範囲または選択肢を()で括る
となるようです。
CNNモデルの構築:
次にモデルを構築します。前回同様MNIST分類用のCNNを使います。このモデルからはベイズ最適化するための評価値となるlossかaccが求められればいいのですが、
・loss
・acc
・model
・history
の4種類を戻り値にしておきました。
model.fit()させてEarlyStoppingで打ち切りになった最後のval_lossとval_accの値を参照しています。
loss = hist.history['val_loss'][-1] acc = hist.history['val_acc'][-1]modelやhistoryは不要ですが、後から参照するかもしれないので一応入れておきました(使うかどうかは分からない)。
return loss, acc, model, hist
上記パイパーパラメータに対応する変数部分には、x[:, 0]などと引数にインデックス番号をつけるようですが、どれが何番目かはわかりにくいのでハイパーパラメータの'name'から参照できる関数をつくってみました。
model.add(Dropout(Param('Dropout_0'), seed=seed))このように書き込めばx[:, 0]へ自動変換してくれます。Hyperoptなどでもディクショナリーのキーを使っていたので、このほうが個人的には使いやすいかと(リスト内容を変えた場合にインデックス番号だと、他の番号も変わってしまうのが面倒なので)。
注意点として、最初に書いたハイパーパラメータはリストであるのに対して、この変数は2次元のndarrayに変換されてから代入されるようです。この変換関数は以下(cnn_model関数内)。
def Param(p_name): p_index = [p['name'] for p in params].index(p_name) p_type = params[p_index]['type'] if type(x) is np.ndarray: if p_type == 'continuous': return float(x[:, p_index]) else: return int(x[:, p_index]) else: # list if p_type == 'continuous': return float(params[p_index]['domain']) else: return int(params[p_index]['domain'])後で最適化されたハイパーパラメータリストを直接渡せるようにしてあります。引数がndarrayならx[:,0]のような2次元ndarray、listならlist内のスカラー値へ変換後代入。また今回の場合、離散値はすべて整数だったのでintかfloatかも振り分けています。
フィッティング関数:
上記CNNモデルを以後のベイズ最適化関数GPyOpt.methods.BayesianOptimization()に直接渡してもいいのですが、CNNモデルからは4種類の値を出力することにしたので、このf(x)関数を間にはさんで必要な評価値だけを渡せるようにしました。今回はaccを評価値として渡すことにし、最小化するためにマイナス反転して-accにしています。
前述のように引数のxは二次元のnumpy.ndarrayになるようです。今回は7種類のハイパーパラメータがあるので、x.shapeは(1,7)になります。最初に設定したハイパーパラメータはリストでありndarrayではないので、この辺をいじる場合は変換するなどの工夫が必要です(このサンプルを参照)。
実際は、
def f(x): x = np.atleast_2d(x) fs = np.zeros((x.shape[0],1)) for i in range(x.shape[0]): loss, acc, model, hist = cnn_model(x) fs[i] += np.log(acc)*(-1) return fsこのように書いたほうがいいのかもしれませんが、戻り値は1次元のndarrayだったので、今回は省略して以下のようにしました。対数変換したほうがいいのかもしれませんが効果の違いは検証していません。
def f(x): loss, acc, model, hist = cnn_model(x) return -acc
ベイズ最適化関数:
GPyOpt.methods.BayesianOptimization()に先程のf(x)関数とハイパーパラメータリストparamsを渡し、その他初期探索値や獲得関数などを決めます。獲得関数はデフォルトではEIになっていますが'EI_MCMC'を選んでみました。'EI_MCMC'を選択する場合は、model_typeで'GP_MCMC'を選んでおかなければいけないようです。
initial_design_numdataは20に設定しましたが、これはどのくらいがいいのかは不明(デフォルト:5)。探索する前のランダムな開始点の数なのかもしれませんが、今回の7次元に対してどのくらいが適当なのか?探索点は徐々に追加されながらフィッティングしていくと思うのでデフォルトの5でもいいのかもしれません。入れた回数だけループするようです(20回で約1時間)。
こまかな設定がいくつかありますが、まだ使いながら試している段階です。
次に、run_optimization(max_iter=50)で最適化が始まります。イテレーションを50回に設定しました。7種類のハイパーパラメータに対してどのくらいが適当なのかはまだ不明(ハイパーハイパーパラメータ)。50回で約4時間かかりました。
ループが終了すれば最適なハイパーパラメータが見つかったことになります。設定した回数より早く終わることもあります。
最適化されたハイパーパラメータの取得:
以下で結果を取得することができます。
x_best = opt.x_opt print([i for i in x_best]) y_best = opt.fx_opt print(y_best)そうすると、
[0.1732254530746627, 0.39555160207057505, 0.14877909656106353, 0.07323704794308367, 128.0, 128.0, 0.1471216716379693] -0.9945388349514563と値が出てきて、最初のリストが最適化された各ハイパーパラメータ。
下の値はそのときのロス値。今回はaccをマイナス反転してあるのでaccの値と同じ。精度0.994以上でているのでまあまあの結果です。
最適化されたハイパーパラメータをモデルに適用:
上記結果と同時にベストモデルやベストウェイトを直接取り出したいのですが、そのような方法がGPyOptにはないようなので、最適化されたハイパーパラメータをCNNモデルに入れ直して再度訓練させてみました。
一応、上記ハイパーパラメータリストを元々のディクショナリー型のリストへ移し替えてからCNNモデルに渡しています。CNNモデルの引数がlistの場合はスカラー値を各変数に代入するような関数にしています。
CNNモデルはEarlyStopping機能をつけているので15ループで収束してくれました(4分25秒)。
このモデルを利用して提出用データを予測します。
まとめと結果(スコア):
最終的にスコアは0.99457でした。まあまあいい結果です(それでも手動調整のベストスコアである0.99528には達していない)。約6時間でこの結果ですが、もっと回せば向上するかはわからないです。これ以上のスコアを出すには、data augmentationでデータを水増しするなど必要かもしれません。
GpyOptはHyperoptに比べるとやや使いにくいという印象でした(サンプルも少ない)。しかしやりたいことに応じて使いやすく改造すればいいのかもしれません。もともとのアルゴリズム自体は優れていると思うので、いくつかを同時に試して結果的にいい方を選ぶ感じでしょうか。時間的にもHyperoptのほうが速いかもしれませんが、どのライブラリであっても数時間はかかるので時間よりも精度がでるほうがいいと思います(仕事で使っているわけではないので)。
このほか気になるライブラリとして、Skopt、Kopt、PyBO、SpearMintなどありますが、とりあえずはもう十分かと。
これまでは機械学習理論やアルゴリズムの種類を覚えていくことが面白かったのですが、Kaggleをきっかけにスコア(精度)を少しでもあげようとすることにも興味を持てたのはよかったです。実際使ってみて、その結果から次にどうすればいいのかという具体的な疑問が次のモチベーションになるので、より理解も深まりつつ面白くなっていく感じです。
追記:
その後、4つのDropout率だけをハイパーパラメータとして最適化した結果スコア:0.99557まで向上(これまでのベストスコアは0.99524)。
その他のハイパーパラメータは以下のように固定。
validation data:test_size=0.15
Dense_0 output units: 256
Dense_1 output units: 128
batch_size=32
そして最適化においては以下の探索回数に設定。
initial_design_numdata=30(2h 29mins)
max_iter=100(stop at 52: 7h 47mins)
max_iterは最大100回に設定しましたが途中52回で収束し停止しました。
合計で10時間30分(GTX1060で)。
関連:
Kaggle Digital Recognizer(MNIST): Hyperopt + Data Augmentation
Kaggle Digital Decognizer(MNIST): Keras, fit_generator() + hyperopt