ChainerとOptunaでハイパーパラメータ探索

## はじめに
今年８月頃からChainerMNをChainerに統合する開発が進められ、10月25日に公開されたChainer5.0から標準で含まれるようになりました。
直前までの予定でChainerMNの環境開発に関して記事を書く予定だったのですが、[ばんくしさんの記事](https://vaaaaaanquish.hatenablog.com/entry/2018/09/17/231640)を発見したため、こちらを参考にしてください。

ということでタイトルにあるように、先日PFNの[公式ブログ](https://research.preferred.jp/2018/12/optuna-release/)で発表されたハイパーパラメータチューニングのライブラリ「Optuna」が公開されました。今回はこちらの実装方法の紹介を行います。

## Optunaとは？

こちら公式ブログからの引用です。

>Optuna はハイパーパラメータの最適化を自動化するためのソフトウェアフレームワークです。ハイパーパラメータの値に関する試行錯誤を自動的に行いながら、優れた性能を発揮するハイパーパラメータの値を自動的に発見します。現在は Python で利用できます。

ハイパーパラメータのチューニングは素早く済ませたいですよね。これまで人力のオペレーションに頼っていた部分をOptunaが補ってくれます。今年の夏のOpen Image Challenge 2018にもモデル構築に貢献していたみたいです。限られた試行回数中で準優勝へ導いたこのライブラリを使ってみようと思います。

## Optunaを実装

今回作成したNotebookは[こちら](https://colab.research.google.com/drive/1yAdWW9opd9jQioCtcnYxdmW5wRCKQv11)になります。今回は犬猫画像の分類を行います。そのためGPUを使えるように設定を行います。以下の図にしたがってバックグラウンドのハードウェアをGPUに変更します。
<img width="950" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/123100/73cc65e0-d437-1bbe-ae6e-d22deec50d39.png">

<img width="993" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/123100/6369cd5f-ecbf-e2ba-7e81-8cd515a9d814.png">


### Chainerのバージョン確認
Optunaを使うためにはChainerのバージョンが4.0.0以上必要です。  
まずはバージョンの確認を行います。現在Google ColaboratoryではデフォルトでChainer5.0.0が用意されています。すごいですね！！

```python
import chainer
```
```python
chainer.print_runtime_info()
>>>Platform: Linux-4.14.65+-x86_64-with-Ubuntu-18.04-bionic
Chainer: 5.0.0
NumPy: 1.14.6
CuPy:
  CuPy Version          : 5.0.0
  CUDA Root             : /usr/local/cuda
  CUDA Build Version    : 9020
  CUDA Driver Version   : 9020
  CUDA Runtime Version  : 9020
  cuDNN Build Version   : 7201
  cuDNN Version         : 7201
  NCCL Build Version    : 2213
iDeep: 2.0.0.post3
```

###　Optunaのインストール
こちらは`!pip`でColabにインストールしましょう。

```python
!pip install optuna
>>>Collecting optuna
...
```
### データのアップロード
今回利用するデータをアップロードしておきましょう。あらかじめGoogle Driveに画像を保存しておき、ColabからMountする方法もありますが、今回はzip形式で圧縮した画像ファイルを直接アップロードします。

以下のコードを実行するとをタブが表示されるのでこちらからファイルをアップロードしましょう。

```python
from google.colab import files
uploaded = files.upload()
```

<img width="436" alt="003.png" src="https://qiita-image-store.s3.amazonaws.com/0/123100/31f0521c-04a2-eb23-5785-927282a4f71c.png">

アップロードが完了したらフォルダ内を確認しましょう。

```python
!ls
>>> dog_cat.zip sample_data
```

続いてzipファイルを解凍します。

```python
!unzip cat_dog.zip
>>>Archive:  dog_cat.zip
   creating: dog_cat/
...
```

解凍されたことを確認して、zipファイルは消去しておきましょう。

```python
!ls
>>>dog_cat dog_cat.zip __MACOSX sample_data

!rm dog_cat.zip
!rm -rf __MACOSX
```

今回読み込んだ画像を確認します。

```python
from PIL import Image
from glob import glob

beef_filepaths = glob('fish_beef/beef/*.jpg')
img = Image.open(beef_filepaths[0])
```

![12.jpg](https://qiita-image-store.s3.amazonaws.com/0/123100/010a8ce1-6edc-95e1-37b5-7614a9cd1098.jpeg)

はい、お肉です。私の大好きなお肉と魚を分類します。

### データセットの作成
ではデータセットの作成を行いましょう。またこのあとChainerを使う際に必要なモジュールもインポートしておきます。

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import warnings
warnings.filterwarnings('ignore')
from PIL import Image
```

そして入力データxと教師データtを作成します。

```python
x, t = [], []

beef_filepaths = glob('beef_fish/beef/*.jpg')
for beef_filepath in beef_filepaths:
    img = Image.open(beef_filepath)
    x.append(np.array(img))
    _t = np.array(0, 'i')
    t.append(_t)

fish_filepaths = glob('beef_fish/fish/*.jpg')
for fish_filepath in fish_filepaths:
    img = Image.open(fish_filepath)
    x.append(np.array(img))
    _t = np.array(1, 'f') 
    t.append(_t)
```

データの確認は忘れずに。

```python
len(x), len(t)
>>>(500, 500)
```

そして、Numpyに変換しChainerの`TupleDataset()`に渡します。

```python
x = np.array(x, 'f')
t = np.array(t, 'i')

dataset = chainer.datasets.TupleDataset(x, t)
n_train = int(len(dataset) * 0.7)
```

## Optunaの準備
### 必要なモジュールの読み込み
必要なモジュールなどをあらかじめ設定しておきます。

```python
import chainer
import chainer.functions as F
import chainer.links as L
​
from chainer.datasets import split_dataset_random
​

n_train = int(len(dataset) * 0.7) #訓練データ
n_train_examples = n_train
n_test_examples = len(dataset) - n_train
batchsize = 32
epoch = 10
```

### モデルの構築
今回はChainerのSequentialモデルでニューラルネットワークの設計を行います。こちらは`layers`というリストに`append()`を使って層を足していくだけでモデルの設計ができる便利なモジュールです。KerasのSequentialモデルと似ています。

ハイパーパラメータは`trial.suggest_○○`として範囲を指定しておくだけになります。例えば`suggest_int()`は指定した範囲の整数を持ち、その中から1~3のいずれかを与えます。連続値は`suggest_○○uniform()`となっています。詳細は[こちら](https://optuna.readthedocs.io/en/stable/reference/trial.html#optuna.trial.Trial.suggest_uniform)を参照してください。

※注意
Sequentialモデルで`chainer.funtions`の呼び出しは関数名のみなので、
`F.max_pooling_2d`などの引数（ksizeやstride）を指定する関数を利用する場合にはpythonの`functools.partial`を呼び出す必要があります。
`functools.partial`は関数の拡張、上書きに使われます。詳細は[こちら]をご覧ください。(https://docs.python.jp/3/library/functools.html#functools.partial)

```python
from functools import partial

def create_model(trial):
    
    # 全結合層の数を保存
    n_fc_layers = trial.suggest_int('n_fc_layers', 1, 3)

    layers = []
    
    layers.append(L.Convolution2D(None, 64, 3, 1, 1))
    layers.append(partial(F.max_pooling_2d, ksize=3, stride=3)) # partial()を使ってksizeとstrideを指定
    
    for i in range(n_fc_layers):
        n_units = int(trial.suggest_loguniform('n_units_l{}'.format(i), 4, 128))
        layers.append(L.Linear(None, n_units))
        layers.append(F.relu)
    layers.append(L.Linear(None, 10))

    return chainer.Sequential(*layers)
```

### 最適化関数の定義
最適化関数をリスト形式で設定することで中からよしなに選択してくれます。またそれぞれのハイパーパラメータも指定した範囲の中からランダムに選択されます。

```python
def create_optimizer(trial, model):
    # 最適化関数の選択
    optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'MomentumSGD'])
    if optimizer_name == 'Adam':
        adam_alpha = trial.suggest_loguniform('adam_alpha', 1e-5, 1e-1)
        optimizer = chainer.optimizers.Adam(alpha=adam_alpha)
    else:
        momentum_sgd_lr = trial.suggest_loguniform('momentum_sgd_lr', 1e-5, 1e-1)
        optimizer = chainer.optimizers.MomentumSGD(lr=momentum_sgd_lr)

    weight_decay = trial.suggest_loguniform('weight_decay', 1e-10, 1e-3)
    optimizer.setup(model)
    optimizer.add_hook(chainer.optimizer.WeightDecay(weight_decay))
    return optimizer
```

### 目的関数の定義

```python
def objective(trial):
    # モデルのインスタンス化
    model = L.Classifier(create_model(trial))
    optimizer = create_optimizer(trial, model) # モデルとoptimizerを紐付ける

    # モデルをGPUに移動
    gpu_id = 0
    model.to_gpu(gpu_id)
    
    # Iteratorの設定
    rng = np.random.RandomState(0)
    train, test = split_dataset_random(dataset, n_train, seed=0)
    train = chainer.datasets.SubDataset(
        train, 0, n_train_examples, order=rng.permutation(len(train)))
    test = chainer.datasets.SubDataset(
        test, 0, n_test_examples, order=rng.permutation(len(test)))
    train_iter = chainer.iterators.SerialIterator(train, batchsize)
    test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)

    # Trainerの設定
    updater = chainer.training.StandardUpdater(train_iter, optimizer, device=gpu_id)
    trainer = chainer.training.Trainer(updater, (epoch, 'epoch'))
    trainer.extend(chainer.training.extensions.Evaluator(test_iter, model, device=gpu_id))
    log_report_extension = chainer.training.extensions.LogReport(log_name=None)
    trainer.extend(chainer.training.extensions.PrintReport(
        ['epoch', 'main/loss', 'validation/main/loss',
         'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
    trainer.extend(log_report_extension)

    # 学習の実行
    trainer.run()

    # 学習結果の保存
    log_last = log_report_extension.log[-1]
    for key, value in log_last.items():
        trial.set_user_attr(key, value)

    # 最終的なバリデーションの値を返す
    val_err = 1.0 - log_report_extension.log[-1]['validation/main/accuracy']
    return val_err
```

### 最適なハイパーパラメータの探索
では、試行回数を指定してハイパーパラメータの探索を行いましょう。

```python
import optuna

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


### 結果の確認
では最後に結果を確認しましょう。`study`に今回の全ての結果が保存されています。もっとも良いaccuracyのハイパーパラメータは`study.best_trial`に含まれています。バリデーションの結果が69%となったのは、全結合層が3層でノードの数は`114->122->127`で最適化関数は`Adam`となりました。αとweight_decayは`5.84e-04`と`8.87e-05`となりました。これは人間の手で調整できそうにないですよね。

```python
print('Number of finished trials: ', len(study.trials))

print('Best trial: ', )
trial = study.best_trial

print('Params: ')
for key, value in trial.params.items():
    print('{}:{}'.format(key, value))
    
print('User attrs: ')
for key, value in trial.user_attrs.items():
    print('{}:{}'.format(key, value))

>>>Number of finished trials:  100
Best trial: 
Params: 
adam_alpha:0.0005836163768560254
weight_decay:8.866268162741139e-05
n_units_l2:127.04057915769907
n_units_l1:122.78511630457464
optimizer:Adam
n_layers:3
n_units_l0:114.39882571222915
User attrs: 
validation/main/loss:1.116943597793579
elapsed_time:2.409521450000284
epoch:10
validation/main/accuracy:0.6920454502105713
main/loss:0.7022001147270203
main/accuracy:0.7698863744735718
iteration:110
```

### 最後に

簡単な形ですが、OptunaをGoogle Colaboratoryで実装しました。
こちらのライブラリはChainerだけではなく、scikit-learn、XGBoost、LightGBMなどの機械学習ライブラリやTensorFlowなどの深層学習フレームワークでも実装可能です。

また学習過程でlossが減少しなくなった場合に枝刈りを行ってくれる[モジュール](https://optuna.readthedocs.io/en/stable/reference/integration.html#)も用意されています。

是非みなさんも使ってみてはいかがでしょうか。

フィードバック、コメントなどいただけると嬉しいです。