書籍「15Stepで踏破 自然言語処理アプリケーション開発入門」をやってみる - 4章Step14メモ「ハイパーパラメータ探索」

# 内容
[15stepで踏破 自然言語処理アプリケーション入門](https://www.amazon.co.jp/dp/4865941320) を読み進めていくにあたっての自分用のメモです。
今回は4章Step14で、自分なりのポイントをメモります。

# 準備
- 個人用MacPC：MacOS Mojave バージョン10.14.6
- docker version：Client, Server共にバージョン19.03.2

# 章の概要
この章では機械学習システムに外部から与えるべき適切なパラメータ（学習するパラメータではない）の値を見つけることを目指す。

- グリッドサーチ
- Hyperopt
    - 確率分布

# 14.1 ハイパーパラメータ
学習によって調整、獲得されるパラメータではなく、学習の前に設計者やプログラマが設定する一段階メタなパラメータ。

- 特徴抽出器
- 識別器
- NN
    - 層の種類
    - 層の数
    - 層ごとのユニット数
    - dropoutの有無と係数
    - optimizerの種類と各種引数
    - 学習率
    - etc...

# 14.2 グリッドサーチ
探索対象のパラメータと、それぞれの値の候補を列挙し、その全ての組み合わせを試してベストのものを見つける手法である。
Scikit-learnでは、`sklearn.model_selection.GridSearchCV`が提供されている。

- Scikit-learnのAPIを持った識別器クラスを用いる
- Kerasの場合はscikit-leran APIラッパーを用いる
- 最適なパラメータは`<gridsearchのインスタンス>.best_params_`で取得できる。

```python
# pipelineを使わない

## train
vectorizer = TfidfVectorizer(tokenizer=tokenize, ngram_range=(1, 2))
train_vectors = vectorizer.fit_transform(train_texts)
parameters = {  # <1>
    'n_estimators': [10, 20, 30, 40, 50, 100, 200, 300, 400, 500],
    'max_features': ('sqrt', 'log2', None),
}
classifier = RandomForestClassifier()
gridsearch = GridSearchCV(classifier, parameters)
gridsearch.fit(train_vectors, train_labels)

## predict
test_vectors = vectorizer.transform(test_texts)
predictions = gridsearch.predict(test_vectors)


# pipelineを使う

## train 
pipeline = Pipeline([
    ('vectorizer', TfidfVectorizer(tokenizer=tokenize, ngram_range=(1, 2))),
    ('classifier', RandomForestClassifier()),
])
parameters = {
    'vectorizer__ngram_range':[(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)],
    'classifier__n_estimators':[10, 20, 30, 40, 50, 100, 200, 300, 400, 500],
    'classifier__max_features':('sqrt', 'log2', None),
}
gridsearch = GridSearchCV(pipeline, parameters)
gridsearch.fit(texts, labels)

## predict
gridsearch.predict(texts)
```

```python:GridSearchCV
# verbose：グリッドサーチの実行状況がわからなかったので表示(1)
# n_jobs：可能な限り(-1)並列で実行
clf = GridSearchCV(pipeline, parameters, verbose=1, n_jobs=-1)
```

```bash:実行結果
from dialogue_agent import DialogueAgent  # <1>
↓
from dialogue_agent_pipeline_gridsearch import DialogueAgent  # <1>

$ docker run -it -v $(pwd):/usr/src/app/ 15step:latest python evaluate_dialogue_agent.py
# 実行完了まで20分ほどかかった
Fitting 3 folds for each of 180 candidates, totalling 540 fits

[Parallel(n_jobs=-1)]: Done  46 tasks      | elapsed:    5.4s
[Parallel(n_jobs=-1)]: Done 196 tasks      | elapsed:  2.2min
[Parallel(n_jobs=-1)]: Done 446 tasks      | elapsed:  5.5min
[Parallel(n_jobs=-1)]: Done 540 out of 540 | elapsed: 19.9min finished

0.7021276595744681
{'classifier__max_features': 'log2', 'classifier__n_estimators': 300, 'vectorizer__ngram_range': (1, 1)}
```

- 探索対象のパラメータが増えてくると、グリッドサーチの探索にかかる時間は指数関数的に長くなってしまう。
- GridSearchCV#fitの内部では、パラメータの組を1つ試すたびに性能評価が行われている
    - クロスバリデーション（デフォルト分割数は3）
    - 学習データをK個に分割しそのうち1個をバリデーションデータ、残り全てを学習データとして学習と評価を行い、それをバリデーションデータを変えながらK回行う評価方法

# 14.3 Hyperoptの利用 14.4 確率分布
グリッドサーチよりも効率よくハイパーパラメータの探索を行うためのツール。
パラメータ空間と目的関数を与えて最適なハイパーパラメータを返す。

- パラメータ空間：探索対象のパラメータとそれぞれの値の候補
    - 一様分布：どの値も出現する確率が全て等しい
    - 乱数一様分布：値の対数が一様分布に従う。大きい値はまばらに、小さい値は密に得ることができる。学習率などは対数の間隔で探索するのが望ましい。
        - 生成：Hyperoptの下限と上限で指定する際は値の対数を指定する。`math.log(..)`
- 目的関数：パラメータ値の組を受け取って値を返す関数
    - 精度を最大化したい場合は、マイナスをかけて最小化する（マイナスの最小化は最大化となる）
- 最適なパラメータは`<hyperoptのインスタンス>.fmin`の返り値から取得できる。

```python
# パラメータ探索
vectorizer = TfidfVectorizer(tokenizer=tokenize, ngram_range=(1, 2))
train_vectors = vectorizer.fit_transform(train_texts)

## 目的関数
def objective(args):
    classifier = RandomForestClassifier(n_estimators=int(args['n_estimators']),
                                        max_features=args['max_features'])
    classifier.fit(tr_vectors, tr_labels)
    val_predictions = classifier.predict(val_vectors)
    accuracy = accuracy_score(val_predictions, val_labels)
    return -accuracy

## パラメータ空間
max_features_choices = ('sqrt', 'log2', None)
space = {
    'n_estimators': hp.quniform('n_estimators', 10, 500, 10),
    'max_features': hp.choice('max_features', max_features_choices),
}
best = fmin(objective, space, algo=tpe.suggest, max_evals=30)

# train
best_classifier = RandomForestClassifier(
    n_estimators=int(best['n_estimators']),
    max_features=max_features_choices[best['max_features']])
best_classifier.fit(train_vectors, train_labels)

# predict
test_vectors = vectorizer.transform(test_texts)
predictions = best_classifier.predict(test_vectors)
```

# 14.5 Kerasへの応用
実行がなかなか終わらないので詳細は省略。

- セッションクリア
    - 探索中に何度もモデルをメモリ上に構築しているとGPUメモリを食いつぶしてしまうため、都度解放処理を入れる
    - `if Keras.backend.backend() == 'tensorflow':`
    - ....`Keras.backend.clear_session()`
- パラメータ
    - 選択肢によって探索項目が異なる場合は、パラメータ空間を入れ子にしてそれぞれの探索項目ごとに詳細項目を指定できる。
    - optimizerの例
        - SGD：learning rateとmomentumを探索
        - Adagrad；leraning rateのみ探索


## 評価
実行がうまく進んでいない（CPUだから時間がかかっているだけ、CPUだと実行できない？）ので、後ほど余裕があったら更新する。