【翻訳】hyperopt チュートリアル

[実数、離散、および条件付きの次元で、厄介な検索空間を最適化するためのPythonライブラリ hyperopt](https://github.com/hyperopt/hyperopt) の チュートリアル([wiki:FMin rev:a663e](https://github.com/hyperopt/hyperopt/wiki/FMin/a663e64546eb5cd3ed462618dcc1e41863ad8688)) を google 翻訳した。 [ライセンス](https://github.com/jaberg/hyperopt/blob/master/LICENSE.txt)

------------------------

このページは、 `hyperopt.fmin()` の基本的な使い方に関するチュートリアルです。
fminが最適化できる目的関数を書く方法と、fminが検索できる検索スペースを記述する方法について説明します。

Hyperoptの仕事は、スカラー値の可能性のある確率関数の最良の値を、その関数の可能な引数の集合よりも見つけ出すことです。 多くの最適化パッケージはこれらの入力がベクトル空間から引き出されると想定していますが、Hyperoptは検索スペースをより詳細に記述することを奨励しています。 あなたの関数が定義されている場所と最適な値がどこにあるかについてより多くの情報を提供することで、hyperoptのアルゴリズムがより効率的に検索できるようになります。

hyperoptを使用する方法は、以下を記述することです：

  * 最小化する目的関数
  * 検索するスペース
  * 検索のポイント評価をすべて格納するデータベース
  * 使用する検索アルゴリズム

この（最も基本的な）チュートリアルでは、デフォルトのトライアルデータベースとダミーランダム検索アルゴリズムを使用して、関数と検索スペースを作成する方法を説明します。 セクション（1）は、目的関数とhyperoptの間の通信のための異なる呼び出し規約についてです。 セクション（2）は検索スペースの記述についてです。

`Trials` データベースを `MongoTrials` データベースに置き換えると、並列検索が可能です。 [パラレル検索にmongodbを使用することに関する別のwikiページがあります。](https://github.com/jaberg/hyperopt/wiki/Parallelizing-Evaluations-During-Search-via-MongoDB)

検索アルゴリズムを選択するのは、 `algo=hyperopt.random.suggest` の代わりに `algo=hyperopt.tpe.suggest` を渡すのと同じくらい簡単です。 検索アルゴリズムは実際に呼び出し可能なオブジェクトであり、そのコンストラクタは構成引数を受け入れますが、それは検索アルゴリズムを選択する仕組みに関するすべてです。

## 1.最小化する関数の定義

Hyperoptは、目的関数を最小化するよう指定する際に、柔軟性や複雑さが増すいくつかのレベルを提供します。
デザイナーとして考えるべき質問は

 * 目的関数の計算中に収集された他の統計情報や診断情報など、関数の戻り値を超えて追加情報を保存しますか？
 * 関数の値以上を必要とする最適化アルゴリズムを使用しますか？
 * 並列プロセス間で通信したいですか？ （例えば、他の作業者、または最小化アルゴリズム）

次のいくつかのセクションでは、1つの変数に対して2次目的関数を最小化する目的関数を実装するさまざまな方法を見ていきます。 各セクションでは、-10から+10までの範囲で検索します。これは *検索スペース* で記述できます。

```
space = hp.uniform('x', -10, 10)
```

Below, Section 2, covers how to specify search spaces that are more complicated.

### 1.1最も単純なケース

hyperoptの最適化アルゴリズムと目的関数の間の通信のための最も簡単なプロトコルは、目的関数が検索空間から有効なポイントを受け取り、そのポイントに関連付けられた浮動小数点 *loss* （別名 否定ユーティリティ）を返します。

```python
from hyperopt import fmin, tpe, hp
best = fmin(fn=lambda x: x ** 2,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    max_evals=100)
print best
```

このプロトコルには、非常に読みやすく、入力するのが簡単であるという利点があります。 ご覧のとおり、ほぼ1ライナーです。 このプロトコルの欠点は、
（1）この種の機能は、各評価についての追加情報を試験データベースに戻すことができないこと、
そして
（2）この種の関数は、検索アルゴリズムまたは他の並行関数評価と相互作用できない。
次の例では、これらのことをやりたい理由がわかります。


### 1.2 トライアルオブジェクトによる追加情報の添付

目的関数が複雑で実行に時間がかかる場合は、最後に出る浮動小数点の損失だけでなく、より多くの統計情報と診断情報を保存したいと思うでしょう。そのような場合、fmin関数は戻り値としてディクショナリを扱えます。あなたの損失関数があなたが望むすべての統計と診断を入れ子にしたディクショナリを返すことができるということです。現実はこれより少し柔軟ではありません。たとえば、mongodbを使用する場合、辞書は有効なJSON文書でなければなりません。それでも、ドメイン固有の補助結果を格納する柔軟性はたくさんあります。

目的関数がディクショナリを返すとき、fmin関数は戻り値の中でいくつかの特殊なキーと値のペアを探し、最適化アルゴリズムに渡します。
必須のKey-Valueペアは2つあります。

  * `status` - `hyperopt.STATUS_STRINGS` のキーの1つです。正常終了のための 'ok'や、関数が定義されていない場合の 'fail'などです。
  * `loss` - 最小化しようとしている浮動小数点関数の値です。ステータスが 'ok'の場合、これは存在しなければなりません。

fmin関数はいくつかのオプションキーにも応答します：

* `attachments` - キーがファイル名のような短い文字列であり、その値がレコードにアクセスするたびにデータベースからロードすべきではない長い文字列（ファイル内容のような）であるキーと値のペアの辞書。 （また、MongoDBは通常のキーと値のペアの長さを制限するので、値がメガバイトになったら attachment にする必要があります）。
* `loss_variance` - float - 確率的目的関数の不確実性
* `true_loss` - float - ハイパーパラメータの最適化を行うときに、モデルの汎化誤差をこの名前で保存すると、組み込みのプロットルーチンからより鮮明な出力を得ることができます。
* `true_loss_variance` - float - 汎化誤差の不確実性

ディクショナリはさまざまなバックエンドストレージメカニズムを使用するため、JSONと互換性があることを確認する必要があります。 ディクショナリ、リスト、タプル、数字、文字列、日時のツリー構造のグラフであれば問題ありません。

**ヒント：** numpy配列を格納するには、それらを文字列に直列化し、それらを添付ファイルとして保存することを検討してください。

上記の関数を辞書を返すスタイルで書くと、次のようになります：

```python
import pickle
import time
from hyperopt import fmin, tpe, hp, STATUS_OK

def objective(x):
    return {'loss': x ** 2, 'status': STATUS_OK }

best = fmin(objective,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    max_evals=100)

print best
```

### 1.3 トライアルオブジェクト

辞書を返す目的を実際に見るには、目的関数を修正していくつかのものを返し、明示的な `trials` 引数を `fmin` に渡しましょう。

```python
import pickle
import time
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials

def objective(x):
    return {
        'loss': x ** 2,
        'status': STATUS_OK,
        # -- store other results like this
        'eval_time': time.time(),
        'other_stuff': {'type': None, 'value': [0, 1, 2]},
        # -- attachments are handled differently
        'attachments':
            {'time_module': pickle.dumps(time.time)}
        }
trials = Trials()
best = fmin(objective,
    space=hp.uniform('x', -10, 10),
    algo=tpe.suggest,
    max_evals=100,
    trials=trials)

print best
```

この場合、fminの呼び出しは前と同じですが、トライアルオブジェクトを直接渡すことで、実験中に計算されたすべての戻り値を検査できます。

したがって、たとえば：

* `trials.trials` - 検索のすべてを表す辞書のリスト
* `trials.results` - 検索中に 'objective'によって返された辞書のリスト
* `trials.losses()` - 損失の浮動小数点リスト（各 'ok' トライアルの）
* `trials.statuses()` - ステータス文字列のリスト

このトライアルオブジェクトは保存したり、組み込みプロットルーチンに渡したり、独自のカスタムコードで分析したりすることができます。

`attachments` は、 `Trials` と `MongoTrials` の両方に同じコードを使用できるようにする特別なメカニズムによって処理されます。

このようなトライアルアタッチメントを取得することができます。これは、5回目のトライアルの 'time_module'アタッチメントを取得します。

```python
msg = trials.trial_attachments(trials.trials[5])['time_module']
time_module = pickle.loads(msg)
```

attachments は大きな文字列であるため、MongoTrialsを使用する場合、必要以上にダウンロードする必要はありません。ストリングは、トライアルを介して試行オブジェクト全体にグローバルにアタッチすることもできます。attachments は、文字列からストリングへの辞書のように動作します。

**N.B.** 現在、Trialsオブジェクトへのトライアル固有の添付ファイルは、同じグローバルトライアルの添付ファイル辞書に入れられますが、将来変更される可能性があり、MongoTrialsには当てはまりません。

### 1.4 MongoDBとのリアルタイムコミュニケーションのためのCtrlオブジェクト

`fmin()` があなたの目的関数に並列実験で使用されるmongodbのハンドルを与えることは可能です。このメカニズムにより、部分的な結果でデータベースを更新し、異なる点を評価している他の並行プロセスと通信することが可能になります。目的関数は、 `random.suggest` のように、新しい検索ポイントを追加することさえできます。

基本的なテクニックは次のとおりです:

  * `fmin_pass_expr_memo_ctrl` デコレータの使用
  * あなた自身の関数で `pyll.rec_eval` を呼び出して、 `expr` と `memo` から検索空間ポイントを構築してください。
  * `hyperopt.Ctrl` のインスタンスである `ctrl` を使用して、他のトライアルオブジェクトと通信します。

この短いチュートリアルでは説明しませんが、私は現在のコードベースで可能なことについて何らかの言及をしたいと思っています。また、hyperoptソース、ユニットテスト、および [hyperopt-convnet](https://github.com/jaberg/hyperopt-convnet) などのサンプルプロジェクトが含まれます。コードのこの部分でスピードアップするために、私に電子メールを送ったり、githubの問題を提出したりしてください。


## 2.検索空間の定義

検索空間は、確率的表現を含むネストされた関数式で構成されます。 確率的表現はハイパーパラメータです。 このネストされた確率的プログラムからのサンプリングは、ランダム探索アルゴリズムを定義する。 ハイパーパラメータ最適化アルゴリズムは、通常の「サンプリング」論理を適応探索戦略に置き換えることによって機能し、検索空間内で指定された分布から実際にサンプリングする試みはしない。

これは、確率的引数サンプリングプログラムとして検索空間を考えるのが一番です。 例えば

```python
from hyperopt import hp
space = hp.choice('a',
    [
        ('case 1', 1 + hp.lognormal('c1', 0, 1)),
        ('case 2', hp.uniform('c2', -10, 10))
    ])
```
このコードを実行した結果は、式識別子とその引数のグラフを参照する変数 `space` です。 実際にサンプリングされたものは何もありません。ポイントをサンプリングする方法を記述した単なるグラフです。 この種の表現グラフを扱うためのコードは `hyperopt.pyll` にあり、これらのグラフを `pyll` グラフまたは *pyllプログラム* と呼んでいます。

必要に応じて、サンプル空間をサンプリングして評価することができます。

```python
import hyperopt.pyll.stochastic
print hyperopt.pyll.stochastic.sample(space)
```

`space` で記述されたこの検索空間には、3つのパラメータがあります:

  * 'a' - ケースを選択する
  * 'c1' - 'case 1'で使用される正の値のパラメータ
  * 'c2' - 'case 2'で使用される有界実数値パラメータ

ここで注意すべき点の1つは、最適化可能な確率的表現のすべてが第1引数として *ラベル* を持つということです。これらのラベルは、呼び出し元にパラメータの選択肢を返すために使用され、内部的にもさまざまな方法で使用されます。

もう1つ注目すべきことは、グラフの中央に（'case 1' と 'case 2' のそれぞれの周りに）タプルを使用したことです。リスト、辞書、およびタプルはすべて「決定論的関数式」にアップグレードされ、探索空間確率的プログラムの一部となります。

3番目に注目すべきは、検索スペースの記述に埋め込まれた `1 + hp.lognormal('c1', 0, 1)` の数値式です。最適化アルゴリズムに関する限り、探索空間に直接1を加え、目的関数自体の論理の中に1を加えることに違いはない。設計者は、このような処理をどこに配置して、必要な種類のモジュール性を実現するかを選択できます。検索スペース内の中間式の結果は、mongodbを使用して並列に最適化する場合でも、任意のPythonオブジェクトにすることができます。探索空間記述に新しいタイプの非確率的表現を追加するのは簡単です（下記2.3節を参照）。

4つ目は、 'c1' と 'c2'が条件パラメータと呼ばれる例であることです。 'c1' と 'c2' のそれぞれは、特定の値の 'a' に対して返されたサンプル内の数字のみを示します。 'a' が 0 の場合、 'c1' は使用されますが 'c2' は使用されません。 'a' が 1 の場合、 'c2' は使用されますが 'c1' は使用されません。 そうすることが理にかなうときは、目的関数内のパラメータを単純に無視するのではなく、このようにパラメータを条件付きのものとしてエンコードする必要があります。 'c1' が（目的関数の引数に影響を与えないため）目的関数に影響を与えないことがあることを明らかにした場合、より効率的に検索できます。

### 2.1パラメータ式

hyperoptの最適化アルゴリズムによって現在認識されている確率的な式は次のとおりです。

* `hp.choice(label, options)`
   * オプションの1つを返します。リストまたはタプルでなければなりません。 `options`の要素は、それ自体が確率的な式で [入れ子] にすることができます。この場合、いくつかのオプションにしか現れない確率的選択が *条件* パラメータになる。

* `hp.randint(label, upper)`
   * [0, upper) の範囲の乱数を返します。この分布のセマンティクスは、より遠い整数値と比較して、近くの整数値間の損失関数に相関がないことです。これは、例えばランダムシードを記述するための適切な分布です。損失関数がおそらく近くの整数値に相関する場合は、`quniform` 、 `qloguniform` 、 `qnormal` または `qlognormal` のいずれかのような「量子化」連続分布の1つを使用するべきです。

* `hp.uniform(label, low, high)`
   * `low` と `high` の間で均等に値を返します。
   * 最適化するとき、この変数は両側の間隔に制約されます。

* `hp.quniform(label, low, high, q)`
    * `round(uniform(low, high) / q) * q` のような値を返します。
    * 目的が幾分「滑らか」であるが、上と下の両方に拘束されるべき離散値に適している。

* `hp.loguniform(label, low, high)`
    * `exp(uniform(low, high))` のような対数に一様に分布するように返します。
    * 最適化すると、この変数は区間 `[exp(low), exp(high)]` に制約されます。

* `hp.qloguniform(label, low, high, q)`
    * `round(exp(uniform(low, high)) / q) * q` のような値を返します。
    * 目的が「滑らか」で、値の大きさがより滑らかになるが、上と下の両方に限定される離散変数に適しています。

* `hp.normal(label, mu, sigma)`
    * 平均ミューと標準偏差 sigma で正規分布している実際の値を返します。最適化する場合、これは制約のない変数です。

* `hp.qnormal(label, mu, sigma, q)`
    * `round(normal(mu, sigma) / q) * q` のような値を返します。
    * おそらく mu 付近の値をとるが、根本的に無制限の離散変数に適しています。

* `hp.lognormal(label, mu, sigma)`
    * 戻り値の対数が正規分布するように `exp(normal(mu, sigma))` に従って描画された値を返します。最適化すると、この変数は正の値に制限されます。

* `hp.qlognormal(label, mu, sigma, q)`
    * `round(exp(normal(mu, sigma)) / q) * q` のような値を返します。
    * 目的が滑らかで、一方の側から境界を定められている変数のサイズによってスムーズになる離散変数に適しています。

### 2.2 A Search Space Example: scikit-learn

これらのすべての可能性を実際に見るには、scikit-learnで分類アルゴリズムのハイパーパラメータのスペースをどのように記述するかを見てみましょう。
(このアイデアは、 [hyperopt-sklearn](https://github.com/hyperopt/hyperopt-sklearn) で開発されています。)

```python
from hyperopt import hp
space = hp.choice('classifier_type', [
    {
        'type': 'naive_bayes',
    },
    {
        'type': 'svm',
        'C': hp.lognormal('svm_C', 0, 1),
        'kernel': hp.choice('svm_kernel', [
            {'ktype': 'linear'},
            {'ktype': 'RBF', 'width': hp.lognormal('svm_rbf_width', 0, 1)},
            ]),
    },
    {
        'type': 'dtree',
        'criterion': hp.choice('dtree_criterion', ['gini', 'entropy']),
        'max_depth': hp.choice('dtree_max_depth',
            [None, hp.qlognormal('dtree_max_depth_int', 3, 1, 1)]),
        'min_samples_split': hp.qlognormal('dtree_min_samples_split', 2, 1, 1),
    },
    ])
```


### 2.3 pyllを使った非確率的表現の追加

pyll関数への引数のようなノードを使うことができます（pyll参照）。
これについてもっと知りたい場合はgithubの問題を提出してください。

簡単に言えば、 `scope`オブジェクトを介して使用できるように、トップレベルの（つまり、pickleに優しい）関数を装飾するだけです。

```python
import hyperopt.pyll
from hyperopt.pyll import scope

@scope.define
def foo(a, b=0):
     print 'runing foo', a, b
     return a + b / 2

# -- this will print 0, foo is called as usual.
print foo(0)

# 検索スペースの説明では、普通のPythonのように `foo`を使うことができます。
# これらの2つの呼び出しは実際にはfooを呼び出さず、
# グラフを評価するためにfooを呼び出す必要があることだけを記録します。

space1 = scope.foo(hp.uniform('a', 0, 10))
space2 = scope.foo(hp.uniform('a', 0, 10), hp.normal('b', 0, 1))

# -- this will print an pyll.Apply node
print space1

# -- this will draw a sample by running foo()
print hyperopt.pyll.stochastic.sample(space1)
```


### 2.4新しい種類のハイパーパラメータの追加

可能であれば、パラメータ探索空間を記述するための新しい種類の確率的表現を追加することは避けなければならない。
すべての検索アルゴリズムがすべてのスペースで動作するためには、検索アルゴリズムはスペースを記述するハイパーパラメータの種類に合致していなければなりません。
ライブラリのメンテナとして、私は表現のいくつかの種類が随時追加されるべきであるという可能性を開いていますが、私が言ったように、私は可能な限りそれを避けたいです。
新しい種類の確率的表現を追加することは、hyperoptが拡張可能である方法の一つではありません。

---------
Copyright (c) 2013, James Bergstra
All rights reserved.