【Optuna】ハイパーパラメータの自動最適化ライブラリの使い方例

# Optuna概要
- PFN様製のハイパーパラメータの自動最適化ライブラリです。
- DeepLearningでもsklearnでも使えます。すごく便利。
- 公式ページはこちらです。
    - https://optuna.readthedocs.io/en/stable/index.html

# 使い方

## 基本形

- 公式チュートリアルから転載

```python
import optuna

def objective(trial):
    x = trial.suggest_float("x", -10, 10)
    return (x - 2) ** 2

study = optuna.create_study()
study.optimize(objective, n_trials=100)

best_params = study.best_params
found_x = best_params["x"]
print("Found x: {}, (x - 2)^2: {}".format(found_x, (found_x - 2) ** 2))

hist_df = study.trials_dataframe(multi_index=True)
hist_df.to_csv("result.csv")
```

1. objectiveを作成
    - objective内で、値を探索したいところに、trial.suggest_xxxと記述。
        - trial.suggest_xxxの種類はいろいろあり後述。
    - objectiveの返り値はスコアとし、optunaはデフォルトではこのスコアを最小する。
1. optuna.create_studyでタスク(study)を作成。
    - 引数にはSamplerを与えることができる。デフォルトはTPESampler。
1. study.optimizeでobjectiveを引数として、n_trialsで試行回数を設定すると最適化が始まる。
1. 結果は、studyの中にbest_paramsとして入っている。
1. study.trials_dataframeで各試行の結果をデータフレームで取得できる。
    - multi_indexはFalseでもよい。データフレームのカラムの見え方が変わるだけ。

## trial.suggest_xxxの種類

|名前|説明|
|:---|:---|
|suggest_categorical|カテゴリ変数。アルゴリズム名やkernel名など各モデルで文字列で表現されるものはこれ。|
|suggest_int|整数パラメータ。log=trueで対数的に分布させられる。stepで刻み幅を指定できる|
|suggest_float|後ろ３つのラッパー。logやstepの設定によりそれぞれが呼ばれる。|
|suggest_uniform|連続一様分布。suggest_floatでlogやstepを指定しない場合。|
|suggest_loguniform|連続対数分布。suggest_floatでlog=trueとした場合。|
|suggest_discreteuniform|離散一様分布。suggest_floatでstepを指定した場合。|

## trial.suggest_xxxの使い方例

- sklearn.svm.SVCの場合

```python
def objective(trial):
  # ...(略)...
  
  params = {
    # 10のべき乗で表すことが多いので、対数一様分布
    "C": trial.suggest_loguniform("C", 0.0001, 1000.0),

    # kernelはカテゴリ変数
    "kernel": trial.suggest_categorical("kernel", ["linear", "poly", "rbf", "sigmoid"]),

    # 整数
    "degree": trial.suggest_int("degree", 1, 10), 

    # gammaはカテゴリと数字双方使える。その場合はとりあえず"numeric"を余分に準備して…
    "gamma": trial.suggest_categorical("gamma", ["scale", "auto", "numeric"]), 
  }
  
  # gammaが"numeric"の場合、ここで"gamma_val"という新しい名前でパラメータを振り、
  # params["gamma"]にはgamma_valにしておく。
  if params["gamma"] == "numeric":
      params["gamma"] = trial.suggest_loguniform("gamma_val", 0.1, 1.0)

  # モデル作成
  model = sklearn.svm.SVC(*params)

  # ...(略)...
```

- カテゴリと数字が混在する、"gamma"の振り方はポイント。(振る意味があるかは置いておいて)
- その他、機械学習のモデルによっては、あるモードでしか使えない引数などもこの方法で回避できる。

## objectiveに引数を与えたい場合
- 公式にあるとおりだが、lambdaを使う方法がシンプルに書ける。
  - https://optuna.readthedocs.io/en/stable/faq.html#how-to-define-objective-functions-that-have-own-arguments

```python
import optuna

# Objective function that takes three arguments.
def objective(trial, min_x, max_x):
    x = trial.suggest_uniform("x", min_x, max_x)
    return (x - 2) ** 2


# Extra arguments.
min_x = -100
max_x = 100

# Execute an optimization by using the above objective function wrapped by `lambda`.
study = optuna.create_study()
study.optimize(lambda trial: objective(trial, min_x, max_x), n_trials=100)
```

## objective内の途中計算結果も取得したい場合
- set_users_attrsを使えば、データフレームに格納することが可能。

```python
def objective(trial):
  # ...(略)...
  
  # user_attrsに設定
  trial.set_user_attr('hoge', hoge)
  trial.set_user_attr('hogehoge', hoge)
  
  # ...(略)...
  
study = optuna.create_study()
study.optimize(objective, n_trials=100)

best_params = study.best_params
found_x = best_params["x"]
print("Found x: {}, (x - 2)^2: {}".format(found_x, (found_x - 2) ** 2))

hist_df = study.trials_dataframe(multi_index=True) # user_attrsに設定したものもDFで取得できる。
hist_df.to_csv("result.csv")
```

## Optunaのログをファイルに出力したい場合
- optunaのロガーを"optuna"で取得できるので、そこにFileHandlerを設定すればよい。

```python
import logging
optuna.logging.get_logger("optuna").addHandler(logging.FileHandler('./optuna.log'))
```

## Optunaのロガーに任意の情報を出力したい場合
- 前項と同様、objective内でロガーを取得すれば可能。

```python
def objective(trial):
    # ロガー取得
    logger = optuna.logging.get_logger("optuna")

    # 出力される
    logger.info("test")
```

## なんか再現性がない場合

- n_jobsを1以外に設定すると、再現性がなくなるように見えている。
- おなじn_jobs(例えば20)を設定していても実行する度に変わるため、再現性がとりたい場合は1にした方が良さそう。

## 多目的化最適化(Multi-objective)
- ここまで目的関数のスコアは1つだった。これをSingle-objectiveと呼んでいる。
- 詳細は以下が詳しい。
    - https://kiseno-log.com/2020/06/07/python-optuna%E3%81%A7%E5%A4%9A%E7%9B%AE%E7%9A%84%E6%9C%80%E9%81%A9%E5%8C%96%E3%82%92%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/
- Samplerも専用のものがあり、結果の見え方はbestを自動で選ぶのではなく、候補をいくつか教えてくれる形式

## その他の参考ページ
- ソースを追うためのミニチュア版optuna、minitunaを使った解説
  - https://cyberagent.ai/optuna-from-scratch
- https://snova301.hatenablog.com/entry/2018/12/14/191025
- https://qiita.com/hiro_o918/items/96edb40756d8adeec11b
- https://woodyzootopia.github.io/2018/12/optuna%E3%81%AE%E8%A8%93%E7%B7%B4%E5%B1%A5%E6%AD%B4%E5%87%BA%E5%8A%9B%E6%A9%9F%E8%83%BD%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E6%8E%A2%E7%B4%A2%E3%81%AE%E6%A7%98%E5%AD%90%E3%82%92%E8%A6%8B%E3%82%8B/
- https://ohke.hateblo.jp/entry/2020/07/04/230000
- https://qiita.com/koshian2/items/1c0f781d244a6046b83e