目次
- 目次
- はじめに
- ハイパーパラメータ学習ライブラリoptuna
- Pythonのコードをoptunaでパラメータ最適化してみる。
- C++のコードをoptunaでパラメータ最適化してみる
- Juliaのコードをoptunaでパラメータ最適化してみる
- 今後optunaに期待すること
- GitHubリポジトリ
- 参考資料
- MyEnigma Supporters
はじめに
最近、最適化技術の勉強の一貫として、
ベイズ統計・最適化を勉強しようと思っています。
そこで、以前PFNが公開した
ベイズ最適化による、ハイパーパラメータ自動最適化ツールである
optunaを使ってみました。
今回の記事では、簡単なoptunaの紹介と、
optunaは、Python製のツールですが、
標準入出力を使えば、任意のプログラミング言語のシステムでも
簡単にパラメータ学習できたので、その方法を紹介したいと思います。
ハイパーパラメータ学習ライブラリoptuna
下記は、optunaのdocを読んだ際の
Twitterのメモです。
とある理由でoptunaデビューしてみた。: pfnet/optuna: A hyperparameter optimization framework https://t.co/6KUAr3RMiM
— Atsushi Sakai (@Atsushi_twi) 2019年1月28日
連続値だけでなく、カテゴリパラメータやIntのパラメータにも対応しているのいいな。使いやすそう。無意味なパラメータは入れないことが重要らしい。optimizeの引数でtimeoutを指定できるのいいな。開発者には嬉しい。Advanced Configurations — Optuna 0.7.0 documentation https://t.co/JqrPQm1cyO
— Atsushi Sakai (@Atsushi_twi) 2019年1月28日
MySQLなどのRDBと連携すると、学習の結果を再利用したり、簡単に並列化できるの面白いな。すごく使いやすい。shellのinterfaceあるのもいい。pythonのsubprocessを使えば、標準入出力経由でJuliaとかC++のシステムの最適化もできたりして。ためしてみよう。
— Atsushi Sakai (@Atsushi_twi) 2019年1月28日
おー、Juliaのコードのパラメータ最適化がoptunaでできたぞ😆。これはブログに書いたほうがいいかも。
— Atsushi Sakai (@Atsushi_twi) 2019年1月28日
1日optunaを使ってみたのですが、
optunaは非常にシンプルで使いやすい印象を持ちました。
ドキュメントも非常に短く、
これを読めば、1時間ほどで使い方を理解することができる気がします。
またMysqlなどのRDBにつながることで、
学習の履歴を再利用したり、並列化を実現するというのは、
ライブラリそのもののコードをシンプルに保ちつつ、
並列化や学習の履歴の再利用が可能になり、
非常に面白い設計だなと感じました。
Pythonのコードをoptunaでパラメータ最適化してみる。
まずはじめに、
Pythonを使ってパラメータ最適化をしてみます。
これはoptunaがPython製のツールなので、
チュートリアルにある方法で簡単に学習できます。
下記のコードは、最適化アルゴリズムでよく利用される
Himmelblau関数の極小値をoptunaで探索するPythonコードです。
""" Himmelblau Function Optimization by python author: Atsushi Sakai """ import optuna import numpy as np import matplotlib.pyplot as plt def HimmelblauFunction(x, y): """ Himmelblau's function see Himmelblau's function - Wikipedia, the free encyclopedia http://en.wikipedia.org/wiki/Himmelblau%27s_function """ return (x**2 + y - 11)**2 + (x + y**2 - 7)**2 def objective(trial): x = trial.suggest_uniform('x', -5, 5) y = trial.suggest_uniform('y', -5, 5) return HimmelblauFunction(x, y) def CreateMeshData(): minXY = -5.0 maxXY = 5.0 delta = 0.1 x = np.arange(minXY, maxXY, delta) y = np.arange(minXY, maxXY, delta) X, Y = np.meshgrid(x, y) Z = [HimmelblauFunction(x, y) for (x, y) in zip(X, Y)] return(X, Y, Z) def main(): print("start!!") # plot Himmelblau Function (X, Y, Z) = CreateMeshData() CS = plt.contour(X, Y, Z, 50) # optimize study = optuna.create_study( study_name="himmelblau_function_opt3", # storage="mysql://root@localhost/optuna" ) study.optimize(objective, n_trials=100, n_jobs=1) print(len(study.trials)) print(study.best_params) for t in study.trials: plt.plot(t.params["x"], t.params["y"], "xb") # plot optimize result plt.plot(study.best_params["x"], study.best_params["y"], "xr") plt.show() print("done!!") if __name__ == '__main__': main()
上記のコードを実行すると、
下記のように、探索結果を表示してくれます。
きちんと最小値付近を探索できていることがわかります。
下記のグラフは別の探索で、optunaが探索した場所を可視化してものです。
最小値付近を重点的に探索しているのがわかります。
これを見ると普通のグリッドサーチに比べると、
効率的に探索できていることがわかります。
C++のコードをoptunaでパラメータ最適化してみる
optunaはPythonのツールですが、
他の言語で書かれたツールを最適化したくなることがあります。
そこで、標準入出力を使って、C++のコードを最適化してみました。
まずはじめに下記のようなC++のコードを準備します。
先程と同じくHimmelblau関数の入力を最小化したいとします。
実行コードの引数を、x,yの入力とし、
出力を標準出力に表示するC++コードを作り、コンパイルしておきます。
#include <iostream> using namespace std; double himmelblau_function(double x, double y) { double t1 = (x * x + y - 11.0); double t2 = (x + y * y - 7.0); return t1 * t1 + t2 * t2; } int main(int argc, char* argv[]) { if (argc >= 3) { double x = std::stod(argv[1]); double y = std::stod(argv[2]); double ans = himmelblau_function(x, y); cout << ans << endl; } }
あとは、下記のようにPythonコードから、
optunaを使ってC++コード内のコードを最適化できます。
具体的には、optunaのパラメータを、
subprocessで実行したC++コードの実行引数に渡し、
返り値を標準出力から取得して、返す形です。
""" Parameter optimization with optuna for Cpp code author: Atsushi Sakai """ import optuna import numpy as np import matplotlib.pyplot as plt import subprocess def HimmelblauFunction(x, y): """ Himmelblau's function see Himmelblau's function - Wikipedia, the free encyclopedia http://en.wikipedia.org/wiki/Himmelblau%27s_function """ return (x**2 + y - 11)**2 + (x + y**2 - 7)**2 def objective(trial): x = trial.suggest_uniform('x', -5, 5) y = trial.suggest_uniform('y', -5, 5) cmd = "./a.out " + str(x) + " " + str(y) d = subprocess.check_output(cmd.split()) return float(d) def CreateMeshData(): minXY = -5.0 maxXY = 5.0 delta = 0.1 x = np.arange(minXY, maxXY, delta) y = np.arange(minXY, maxXY, delta) X, Y = np.meshgrid(x, y) Z = [HimmelblauFunction(x, y) for (x, y) in zip(X, Y)] return(X, Y, Z) def main(): print("start!!") # plot Himmelblau Function (X, Y, Z) = CreateMeshData() CS = plt.contour(X, Y, Z, 50) # optimize study = optuna.create_study( study_name="julia_himmelblau_function_opt", # storage="mysql://root@localhost/optuna" ) # study.optimize(objective, n_jobs=1) study.optimize(objective, n_trials=100, n_jobs=1) print(len(study.trials)) print(study.best_params) # plot optimize result plt.plot(study.best_params["x"], study.best_params["y"], "xr") plt.show() print("done!!") if __name__ == '__main__': main()
この方法では、
C++に実行ファイルと標準入出力経由でデータをやりとりするので、
処理が遅いかなと思いましたが、先程のpure Pythonの場合と比べて
感覚的にほとんど差がありませんでした。
本当は標準入出力経由でデータをやりとりするのではなく、
pybind11などを使って、C++コードを呼ぶべきだと思うのですが、
C++の処理が重い場合は、標準入出力のオーバーヘッドは無視できると思うので、
ほとんどすべてのプログラミング言語で利用できる
統一的な手法としては良い方法だと思います。
Juliaのコードをoptunaでパラメータ最適化してみる
次はJuliaのコードを最適化してみます。
作戦は先程のC++のコードと同じで、
標準入出力経由でJuliaのコードとPythonのコードが、
入力データと、目的関数の結果をやり取りすることで
最適化を実施します。
下記がJuliaのコードです。
" Himmelblau function author: Atsushi Sakai " function himmelblau_function(x, y) return (x^2 + y - 11)^2 + (x + y^2 - 7)^2 end x = parse(Float64,ARGS[1]) y = parse(Float64,ARGS[2]) println(himmelblau_function(x, y))
下記はpythonコードです。
subprocess経由で先程のjuliaコードを実行しています。
import optuna import numpy as np import matplotlib.pyplot as plt import subprocess def HimmelblauFunction(x, y): """ Himmelblau's function see Himmelblau's function - Wikipedia, the free encyclopedia http://en.wikipedia.org/wiki/Himmelblau%27s_function """ return (x**2 + y - 11)**2 + (x + y**2 - 7)**2 def objective(trial): x = trial.suggest_uniform('x', -5, 5) y = trial.suggest_uniform('y', -5, 5) cmd = "julia himmelblau_function.jl " + str(x) + " " + str(y) d = subprocess.check_output(cmd.split()) return float(d) def CreateMeshData(): minXY = -5.0 maxXY = 5.0 delta = 0.1 x = np.arange(minXY, maxXY, delta) y = np.arange(minXY, maxXY, delta) X, Y = np.meshgrid(x, y) Z = [HimmelblauFunction(x, y) for (x, y) in zip(X, Y)] return(X, Y, Z) def main(): print("start!!") # plot Himmelblau Function (X, Y, Z) = CreateMeshData() CS = plt.contour(X, Y, Z, 50) # optimize study = optuna.create_study( study_name="julia_himmelblau_function_opt", # storage="mysql://root@localhost/optuna" ) # study.optimize(objective, n_jobs=1) study.optimize(objective, n_trials=100, n_jobs=1) print(len(study.trials)) print(study.best_params) # plot optimize result plt.plot(study.best_params["x"], study.best_params["y"], "xr") plt.show() print("done!!") if __name__ == '__main__': main()
残念ながら、この手法では
評価のたびに、juliaを起動し直すので、
juliaの起動が遅いという問題で、
かなり学習にオーバーヘッドがあります。
学習中は、同じjuliaのVMを利用するなどの方法が重要で、
下記のpyjuliaのようなライブラリを使って、
JuliaコードとPythonコードを連携することが重要だと思います。
しかし、juliaの処理が非常に長い場合は
このオーバーヘッドも無視できると思うので、
一つの手法としてはありだと思います。
今後optunaに期待すること
最後に、まだoptunaはv0.7と、
進化途中のツールなので、いくつか期待することをメモしておきます。
(余裕があればPRしたいです)
create_studyとstudyの関数の統合
おそらく、RDBとのデータのやり取りの関係で、
studyを初期化する関数create_studyと、
一度学習した結果を利用するstudyの関数が別れていますが、
キーワード引数 init = true or falseなどを使って、
この関数を統合してもらえると、システムのAPIがよりシンプルになり、
見通しが良くなる気がしました。
他の言語との統合方法のexampleの拡充
先程紹介したpybind11やpyjuliaなどを使った、
他の言語のシステムを最適化するためのexampleコードが増えると、
pythonユーザ以外の人にも、optunaが広がると思いました。
GitHubリポジトリ
今回紹介したコードはすべて下記で公開しています。
参考資料
MyEnigma Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。