はじめに
今年8月頃からChainerMNをChainerに統合する開発が進められ、10月25日に公開されたChainer5.0から標準で含まれるようになりました。
直前までの予定でChainerMNの環境開発に関して記事を書く予定だったのですが、ばんくしさんの記事を発見したため、こちらを参考にしてください。
ということでタイトルにあるように、先日PFNの公式ブログで発表されたハイパーパラメータチューニングのライブラリ「Optuna」が公開されました。今回はこちらの実装方法の紹介を行います。
Optunaとは?
こちら公式ブログからの引用です。
Optuna はハイパーパラメータの最適化を自動化するためのソフトウェアフレームワークです。ハイパーパラメータの値に関する試行錯誤を自動的に行いながら、優れた性能を発揮するハイパーパラメータの値を自動的に発見します。現在は Python で利用できます。
ハイパーパラメータのチューニングは素早く済ませたいですよね。これまで人力のオペレーションに頼っていた部分をOptunaが補ってくれます。今年の夏のOpen Image Challenge 2018にもモデル構築に貢献していたみたいです。限られた試行回数中で準優勝へ導いたこのライブラリを使ってみようと思います。
Optunaを実装
今回作成したNotebookはこちらになります。今回は犬猫画像の分類を行います。そのためGPUを使えるように設定を行います。以下の図にしたがってバックグラウンドのハードウェアをGPUに変更します。
Chainerのバージョン確認
Optunaを使うためにはChainerのバージョンが4.0.0以上必要です。
まずはバージョンの確認を行います。現在Google ColaboratoryではデフォルトでChainer5.0.0が用意されています。すごいですね!!
import chainer
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にインストールしましょう。
!pip install optuna
>>>Collecting optuna
...
データのアップロード
今回利用するデータをアップロードしておきましょう。あらかじめGoogle Driveに画像を保存しておき、ColabからMountする方法もありますが、今回はzip形式で圧縮した画像ファイルを直接アップロードします。
以下のコードを実行するとをタブが表示されるのでこちらからファイルをアップロードしましょう。
from google.colab import files
uploaded = files.upload()
アップロードが完了したらフォルダ内を確認しましょう。
!ls
>>> dog_cat.zip sample_data
続いてzipファイルを解凍します。
!unzip cat_dog.zip
>>>Archive: dog_cat.zip
creating: dog_cat/
...
解凍されたことを確認して、zipファイルは消去しておきましょう。
!ls
>>>dog_cat dog_cat.zip __MACOSX sample_data
!rm dog_cat.zip
!rm -rf __MACOSX
今回読み込んだ画像を確認します。
from PIL import Image
from glob import glob
beef_filepaths = glob('fish_beef/beef/*.jpg')
img = Image.open(beef_filepaths[0])
はい、お肉です。私の大好きなお肉と魚を分類します。
データセットの作成
ではデータセットの作成を行いましょう。またこのあとChainerを使う際に必要なモジュールもインポートしておきます。
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')
from PIL import Image
そして入力データxと教師データtを作成します。
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)
データの確認は忘れずに。
len(x), len(t)
>>>(500, 500)
そして、Numpyに変換しChainerのTupleDataset()
に渡します。
x = np.array(x, 'f')
t = np.array(t, 'i')
dataset = chainer.datasets.TupleDataset(x, t)
n_train = int(len(dataset) * 0.7)
Optunaの準備
必要なモジュールの読み込み
必要なモジュールなどをあらかじめ設定しておきます。
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()
となっています。詳細はこちらを参照してください。
※注意
Sequentialモデルでchainer.funtions
の呼び出しは関数名のみなので、
F.max_pooling_2d
などの引数(ksizeやstride)を指定する関数を利用する場合にはpythonのfunctools.partial
を呼び出す必要があります。
functools.partial
は関数の拡張、上書きに使われます。詳細は[こちら]をご覧ください。(https://docs.python.jp/3/library/functools.html#functools.partial)
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)
最適化関数の定義
最適化関数をリスト形式で設定することで中からよしなに選択してくれます。またそれぞれのハイパーパラメータも指定した範囲の中からランダムに選択されます。
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
目的関数の定義
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
最適なハイパーパラメータの探索
では、試行回数を指定してハイパーパラメータの探索を行いましょう。
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
となりました。これは人間の手で調整できそうにないですよね。
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が減少しなくなった場合に枝刈りを行ってくれるモジュールも用意されています。
是非みなさんも使ってみてはいかがでしょうか。
フィードバック、コメントなどいただけると嬉しいです。