optunaをskoptと比較しながら使ってみた

こんにちは、shiibassです。
PFNがハイパーパラメータの最適化フレームワークoptunaを公開しました。
https://preferredresearch.jp/2018/12/03/optuna-release/
私はハイパーパラメータの最適化ではなくブラックボックス関数の最適化タスクを頻繁に計算する必要があり若干違和感のある使い方をしますが、本来の目的としてはoptunaはchainerでディープラーニングするときのパラメータチューニングと相性が特に良いみたいです。ディープラーニングする人は一度触ってみてほしいです。

## サンプルコード

ちょうどskoptを使おうとしていてそのコードがあったので、比較しながら記します。サンプルコード全体は
https://github.com/shiibashi/sample/blob/master/bays.ipynb
と
https://github.com/shiibashi/sample/blob/master/optuna.ipynb
にあります。

```python
obtuna==0.4.0
scikit-optimize==0.5.2
```
です。

### データ
データを適当に作りました。

```python

df = pandas.DataFrame({"x": numpy.random.rand(100)})
df["y"] = -df["x"] * df["x"] + 4
df["z"] = df["y"] * 2 + df["x"]
```

### 変数設定
最適化する関数の変数を設定します。
skoptでは以下のように整数変数、実数変数を定義します。

```python
# skopt
from skopt.space import Real, Integer
from skopt.utils import use_named_args

space = [
    Integer(0, 1, name="intercept"),
    Real(0, 1, "uniform", name="dummy"),
    Real(1, 5, "uniform", name="scale"),    
]
```

一方optunaではこのようになります。呼び出し方が違うだけでほぼ同じです。

```python
def objective(trial):
    intercept = trial.suggest_int("intercept", 0, 1)
    dummy = trial.suggest_uniform("dummy", 0, 1)
    scale = trial.suggest_uniform("scale", 1, 5)
```

なお、設定可能な変数はTrialクラスを参照してください。
https://github.com/pfnet/optuna/blob/b07b0c04dec9300cea3d4222701cfe35e58680b5/optuna/trial.py#L16
読んだ限りだと以下の5つがありました。
suggest_uniform(self, name, low, high),
suggest_loguniform(self, name, low, high),
suggest_discrete_uniform(self, name, low, high, q),
suggest_int(self, name, low, high),
suggest_categorical(self, name, choices),
これらはハイパーパラメータの値の制約と分布を設定します。uniformは一様分布、loguniformは対数をとって一様分布でパラメータを取得するということだと思います。

### 目的関数設定

skoptを使う場合はこんな感じです。線形回帰のスコアを最大化（負値にして最小化）しています。データにパラメータを掛けたり、足したりしていますがノイズを与えているだけでパラメータ最適化の観点からは特に意味のない処理です。

```python
# skopt
from skopt.utils import use_named_args
from skopt import gp_minimize
from skopt import forest_minimize

def opt_fit(space):
    @use_named_args(space)
    def my_objective(**params):
        obj = _objective(**params)
        return -obj
    gp = forest_minimize(func=my_objective, dimensions=space)
    return gp
    
def _objective(**params):
    df2 = df.copy()
    lm = LinearRegression(fit_intercept=params["intercept"])
    df2["z"] = df2["z"] + numpy.random.rand(len(df2)) * params["scale"] + params["dummy"]
    lm.fit(df2[["x", "y"]], df2["z"])
    obj = lm.score(df2[["x", "y"]], df2["z"])
    return obj
```

optunaではこう書きます。skoptに比べて書きやすいですね。optunaでも目的関数の最小化を行いますので、maximizeが目的ならマイナス1を掛けるのを忘れないようにしましょう。

```python
def objective(trial):
    intercept = trial.suggest_int("intercept", 0, 1)
    dummy = trial.suggest_uniform("dummy", 0, 1)
    scale = trial.suggest_uniform("scale", 1, 5)
    df2 = df.copy()
    lm = LinearRegression(fit_intercept=intercept)
    df2["z"] = df2["z"] + numpy.random.rand(len(df2)) * scale + dummy
    lm.fit(df2[["x", "y"]], df2["z"])
    obj = lm.score(df2[["x", "y"]], df2["z"])
    return -obj
```

### 最適化

skopt

```python
g = opt_fit(space)
#結果
g.fun # -0.6516630946682641
g.x # [0, 0.44215255958028, 1.0729714092421192]
```

optuna

```python
study = optuna.create_study()
study.optimize(objective, n_trials=100)
# 結果
study.best_trial
# FrozenTrial(trial_id=95, state=<TrialState.COMPLETE: 1>, value=-0.033806141265586565, datetime_start=datetime.datetime(2018, 12, 4, 22, 52, 0, 10732), datetime_complete=datetime.datetime(2018, 12, 4, 22, 52, 0, 31675), params={'intercept': 0, 'dummy': 0.9757316027924707, 'scale': 4.421644328196012}, user_attrs={}, system_attrs={}, intermediate_values={}, params_in_internal_repr={'intercept': 0, 'dummy': 0.9757316027924707, 'scale': 4.421644328196012})
study.best_params
# {'dummy': 0.9757316027924707, 'intercept': 0, 'scale': 4.421644328196012
```

反復回数が少ないのとデータにノイズを与えているために精度の比較は無意味ですので注意してください。

## optunaのメリット

サンプルコードでは特にoptunaである必要性はありません。optunaをぜひ使ってほしい理由としては以下があります。

### chainerと相性が良い
PFNが開発しているので当たり前ですが、相性がいいようです。ディープラーニングのフレームワークはいくつかありますが、define-by-runとdefine-and-runという2種類の実行方式があって、それぞれメリットデメリットがありますが私はあまり気にしていないです。ただ、以下の記事によると今までのディープラーニングのハイパーパラメータ最適化のフレームワークはdefine-and-runが多かったそうですので、差別化はできています。https://preferredresearch.jp/2018/12/03/optuna-release/
chainerMNが高速化で有名ですが、理論研究をするような人でないならそこまでシビアになる必要もないと思いますが皆さんはどう思うのでしょうか？こういう会話をする相手もいないので機械学習をやっている人の意見を聞いてみたいものです。

### 分散最適化
最適化は機械学習の「学習」で動いている処理です。最適化計算を分散で実行できるようになっていて要は高速で処理できるようになっているということです。愚直な実装だと全探索、よく使われているイメージのあるものだとベイズ最適化、ニューラルネットワークの最適化だと強化学習を使ってネットワークそのものも変数にする方法もあったりしますが、optunaではどのようなアルゴリズムが実装されているのか確認していません。確認していませんが、使う分にはあまり問題にはならない気がします。

### RDB連結性
どうやらRDBと連結させて最適化のセーブ/ロードができるみたいです。インメモリデータベースが対応していると書いてあります。あとリモートサーバのRDBにも結果を出力できるっぽい？です。skoptでは各反復の計算結果をメモリ上に保管するのですが、反復回数や変数を増やしまくるとメモリ不足になってしまうのですが、optunaではその辺のメモリ効率はどうなっているのか確認することはできませんでした。たぶんoptunaを使う人は多いでしょうからすぐに誰かが記事にまとめてくれると思います。

## 最後に
optunaをskoptと比較しながら使ってみました。最適化の経験がなくてもすぐに使えるようになるはずですのでぜひ使ってみましょう。
ただし、こういったフレームワークや論文のアルゴリズムを実装して使うような立場からすると、必ずしも使わなくてもいいのかなあとは思っています。というのも身につけなくてもAWS, GCPなどのクラウドですぐに実装されますよね。それを待つのも一つの手ですが、パラメータの最適化というものがあることは知っておきたいです。
一見機械学習は難しそうですが、基礎技術の研究をしないなら細かいところまでわからなくても機械学習は始められますし、今回のoptunaに関してもドキュメントを読んで引数に合うようにデータを設定したくらいですので、とりあえず触ってみるのスタンスで積極的に触ってみましょう。世の中の機械学習技術の需要は非常に高いですが、クラウドの機械学習サービスのドキュメントを読んで使えれば対応できるようなタスクが大部分です。ついでに大学で情報数学を勉強していなくても機械学習をばりばり触っている人も多いですし、qiitaには山のように機械学習記事があります。環境としては十分すぎるほど整っているので新しい技術に挑戦したい人は選択肢の一つに機械学習を入れていただけると幸いです。