MyEnigma

とあるエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

Python製ハイパーパラメータ学習ライブラリoptunaを使って様々な言語のコードを最適化する方法

目次

はじめに

最近、最適化技術の勉強の一貫として、

ベイズ統計・最適化を勉強しようと思っています。

そこで、以前PFNが公開した

ベイズ最適化による、ハイパーパラメータ自動最適化ツールである

optunaを使ってみました。

github.com

 

今回の記事では、簡単なoptunaの紹介と、

optunaは、Python製のツールですが、

標準入出力を使えば、任意のプログラミング言語のシステムでも

簡単にパラメータ学習できたので、その方法を紹介したいと思います。

ハイパーパラメータ学習ライブラリoptuna

下記は、optunaのdocを読んだ際の

Twitterのメモです。

 

1日optunaを使ってみたのですが、

optunaは非常にシンプルで使いやすい印象を持ちました。

ドキュメントも非常に短く、

これを読めば、1時間ほどで使い方を理解することができる気がします。

optuna.readthedocs.io

 

またMysqlなどのRDBにつながることで、

学習の履歴を再利用したり、並列化を実現するというのは、

ライブラリそのもののコードをシンプルに保ちつつ、

並列化や学習の履歴の再利用が可能になり、

非常に面白い設計だなと感じました。

myenigma.hatenablog.com

 

Pythonのコードをoptunaでパラメータ最適化してみる。

まずはじめに、

Pythonを使ってパラメータ最適化をしてみます。

これはoptunaがPython製のツールなので、

チュートリアルにある方法で簡単に学習できます。

下記のコードは、最適化アルゴリズムでよく利用される

Himmelblau関数の極小値をoptunaで探索するPythonコードです。

en.wikipedia.org

""" 

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_sample/Figure_1.png at master · AtsushiSakai/optuna_sample

 

下記のグラフは別の探索で、optunaが探索した場所を可視化してものです。

最小値付近を重点的に探索しているのがわかります。

これを見ると普通のグリッドサーチに比べると、

効率的に探索できていることがわかります。

optuna_sample/Figure_2.png at master · AtsushiSakai/optuna_sample

 

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++コードを呼ぶべきだと思うのですが、

myenigma.hatenablog.com

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コードを連携することが重要だと思います。

github.com

 

しかし、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リポジトリ

今回紹介したコードはすべて下記で公開しています。

github.com

参考資料

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com