scikit-optimizeのEarlyStopperで最適化を中断する

# 前置き
前回記事があります。
[Pythonでめっちゃ簡単にできるベイズ最適化](https://qiita.com/saiaron/items/997b330c0e77d6a18f1f)

長い間、サンプリングを続ければ良いものの、一定の評価値に達したら中断したい場合、`scikit-optimize`に用意されているCallbackで止めるようにしないと、あらかじめ指定したサンプリング回数が終わるまで止められません。
プロセスキルすれば止まるけど、そこまでの結果を受け取って取り扱うことができない。

というわけで、`scikit-optimize.callbacks.EarlyStopper`を使って、任意の条件で最適化を中断したいが、いかんせんこのEarlyStopperは使い方がろくに説明されていない。
help()で見ろとか書いてあるんだが、出てくるのは下記のリンクと同レベル。

というわけで、本記事では日本語で使い方を記録しておきたいと思います。

> [skopt.callbacks.EarlyStopper](https://scikit-optimize.github.io/stable/modules/generated/skopt.callbacks.EarlyStopper.html)

```python
In [0]:from skopt.callbacks import EarlyStopper
In [1]:help(EarlyStopper)
Help on class EarlyStopper in module skopt.callbacks:

class EarlyStopper(builtins.object)
 |  Decide to continue or not given the results so far.
 |  
 |  The optimization procedure will be stopped if the callback returns True.
 |  
 |  Methods defined here:
 |  
 |  __call__(self, result)
 |      Parameters
 |      ----------
 |      result : `OptimizeResult`, scipy object
 |          The optimization as a OptimizeResult object.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
```

# ソースコード全文
[前回](https://qiita.com/saiaron/items/997b330c0e77d6a18f1f)でも解説しているので、変更のある部分で抜粋して解説する。

```python
import numpy as np
from skopt import gp_minimize
from skopt.callbacks import EarlyStopper

class Stopper(EarlyStopper):
    def __call__(self, result):
        ret = False
        if result.fun < -1.0:
            ret = True
        return ret

def func(param=None):
    ret = np.cos(param[0] + 2.34) + np.cos(param[1] - 0.78)
    return -ret

if __name__ == '__main__':
    x1 = (-np.pi, np.pi)
    x2 = (-np.pi, np.pi)
    x = (x1, x2)
    result = gp_minimize(func, x, 
                          n_calls=30,
                          noise=0.0,
                          model_queue_size=1,
                          callback=[Stopper()],
                          verbose=True)
```

## EarlyStopperの継承クラス

```python
from skopt.callbacks import EarlyStopper

class Stopper(EarlyStopper):
    def __call__(self, result):
        ret = False
        if result.fun < -1.0:
            ret = True
        return ret
```
なんとなく、予想はできたけど、継承して自前で実装せよということだった。
というわけで、EarlyStopperをインポートして、自作クラスStopperを作っている。
コンストラクタとかは分からないけどオーバーライドする必要なし。
`__call__`をオーバーライドするだけでよい。
この時、`__call__`に`result`という引数を設けているが、最適化の最中に都度都度の最新の`result`が渡されてくる。
今回はこの`result`の中にある`fun`というメンバーを参照し、この値が-1.0を下回っていたらTrue、そうでなければFalseを返す仕組みにした。
**この`__call__`がTrueを返すと中断、Falseなら続行なのである。**

## gp_minimizeへ設定

```python
    result = gp_minimize(func, x, 
                          n_calls=30,
                          noise=0.0,
                          model_queue_size=1,
                          callback=[Stopper()],
                          verbose=True)
```
[前回](https://qiita.com/saiaron/items/997b330c0e77d6a18f1f)では設定しなかった`callback`という引数にStopperクラスのインスタンスを渡しておく。
この時、Listで渡しているのは複数のCallbackを受け付けることができるようになっているため。
一つでもListで渡しておく。
それ以外は前回のまま。
これでサンプリング回数30回以下で止まればよいが、当然-1.0という閾値に引っかからなければ止まらない。
※が、問題が簡単すぎるので、絶対にイケる。

# 結果

```python
Iteration No: 1 started. Evaluating function at random point.
Iteration No: 1 ended. Evaluation done at random point.
Time taken: 0.0000
Function value obtained: -0.9218
Current minimum: -0.9218
Iteration No: 2 started. Evaluating function at random point.
Iteration No: 2 ended. Evaluation done at random point.
Time taken: 0.0000
Function value obtained: 0.9443
Current minimum: -0.9218
Iteration No: 3 started. Evaluating function at random point.
Iteration No: 3 ended. Evaluation done at random point.
Time taken: 0.0000
Function value obtained: 1.6801
Current minimum: -0.9218
Iteration No: 4 started. Evaluating function at random point.
Iteration No: 4 ended. Evaluation done at random point.
Time taken: 0.0000
Function value obtained: -0.0827
Current minimum: -0.9218
Iteration No: 5 started. Evaluating function at random point.
Iteration No: 5 ended. Evaluation done at random point.
Time taken: 0.0000
Function value obtained: -1.1247
Current minimum: -1.1247
Iteration No: 6 started. Evaluating function at random point.
```
5回目のサンプリングで評価値が-1.1247と-1.0を下回ったので、6回目をやろうとして止まった。
成功。
まぁ、絶対にひっかかるであろう閾値にはしたが・・・

# 蛇足
他にも`scikit-optimize`にはいろいろなCallbackが用意されているが、**任意の条件で**となるとこの`EarlyStopper`しかない。
で、多分継承して自前で実装しなければならないのも、この`EarlyStopper`だけと思う。
他のはインスタンス作成時にパラメータを設定しておけば、今回のもの同様に設定できるはず。
ちょっとどう考えればよいかわかりにくいので、使ってない・・・
>[skopt.callbacks](https://scikit-optimize.github.io/stable/modules/callbacks.html#callbacks)