LoginSignup
100
83

More than 3 years have passed since last update.

Optunaを用いたPyTorchにおけるハイパーパラメータチューニング

Last updated at Posted at 2019-07-02

初投稿となります.

はじめに

DeepLearningの重要な要素の一つとしてハイパーパラメータがあります.
重みといった勾配法などのアルゴリズムによって最適化を行うものがパラメータであることに対し,人の手によって最適化を行うものがハイパーパラメータとなります.ハイパーパラメータは,最適化手法やその学習率,ニューロンの数やその層数,活性化関数,ドロップアウト率など様々なものが挙げられ,これを手動で調整するとなると試行錯誤を繰り返さなければなりません.

そこで登場するのが「Optuna」です.

今回は,PyTorchによるMNISTの画像認識を通して,このOptunaの使い方の説明をしたいと思います.

また,Optunaについて調査した際,PyToch上で畳み込み層数のチューニングを行なったという記事が見当たらなかったため,せっかくなので「畳み込み層数」をはじめとするハイパーパラメータのチューニングを行いたいと思います.

Optunaとは

Optunaとは,Preferred Networks発の 機械学習におけるハイパーパラメータの自動最適化(チューニング)を行うフレームワークのことです.
目的関数に対して,適当なハイパーパラメータを使って評価を繰り返し,その目的関数が最小となる最適なハイパーパラメータを探し出します.

Optunaの使い方

Optunaの使用方法です.
詳しく知りたい方は,公式ドキュメントをご覧ください.

インストール

Optunaはpipコマンドによりインストールすることができます.

pip install optuna

目的関数の定義

上記したように,まず最小化する目的関数を設定するのですが,これはobjective()という関数で定義します.引数としてtrialオブジェクトを与えます.

def objective(trial):

    return 

trialオブジェクトの定義

trialオブジェクトは,Optunaに実装されているクラスのインスタンスであり,最適化したいハイパーパラメータの定義に用います.適当な範囲から値をサンプリングしてくるのですが,過去に探索したパラメータの情報を保持し,それを元に次に探索する値を決定します.

trialの定義ですが,以下のように行うことができます.

#カテゴリの試行を行うパラメータ
param1 = trial.suggest_categorical(name, choices)

#整数値の試行を行うパラメータ
param2 = trial.suggest_int(name, low, high)

#連続値の試行を行うパラメータ
param3 = trial.suggest_uniform(name, low, high)

#離散値の試行を行うパラメータ
param4 = trial.suggest_discrete_uniform(name, low, high, q)

#対数値の試行を行うパラメータ
param5 = trial.suggest_loguniform(name, low, high)

nameはstr型でありパラメータの名前を指定し,choicesはlist型であり複数のカテゴリ名の選択肢として提示する引数となります.lowhighではパラメータの最小値と最大値を提示し,qによってその値間を試行する間隔を設定します.

他にも様々なメソッドがありますが,今回は実例で使用するもののみを紹介しました.

studyオブジェクトの定義

ハイパーパラメータを求めるにはstudyオブジェクトを作る必要があります.
このstudyオブジェクトに最適化の結果が保持されます.

study = optuna.creat_study()

そして,optimizeメソッドを用いることにより,最適なハイパーパラメータを求めることができます.

study.optimize(objective, n_trials = 100)

optimizeメソッドの第1引数は目的関数であるobjective関数となり,第2引数は試行回数となります.tudyオブジェクト内でtrialの処理が行われるため,optimizeメソッドを実行するだけで自動的に目的関数の最小値およびそのハイパーパラメータを探してくれます.

最適化の結果は以下で見ることができます.

#最適化したハイパーパラメータの結果
study.best_params

#最適化後の目的関数の値
study.best_value

#全試行過程
study.trials

簡単ではありましたが,使用方法の説明は以上です.

実例_MNIST画像認識のハイパーパラメータチューニング

それでは,実際にMNISTの画像認識を通してハイパーパラメータのチューニングを行いたいと思います.

実行環境

  • Python : 3.6.8
  • PyTorch : 0.4.1
  • Optuna : 0.12.0

チューニングを行うハイパーパラメータ

  • 畳み込み層の数(3 ~ 7)
  • 各畳み込み層のフィルタ数(16, 32, 48, ..., 128)
  • 全結合層のユニット数(100, 200, 300, 400, 500)
  • 活性化関数(ReLU, ELU)
  • 最適化手法(Adam, MomentumSGD, rmsprop)
  • 学習率(adam_lr(1e-10 ~ 1e-3), momentum_sgd_lr(1e-5 ~ 1e-1))
  • weight_decay(1e-10 ~ 1e-3)

パラメータチューニングを行うにあたって,自分は「公式リポジトリ」と「PyTorch+OptunaでMNIST」の2つを参考にさせていただきました.こちらもぜひ見てみてください.

今回のコードはGoogle Driveにアップロードしてあるので,実際に動かしたい方はそちらを参照してください.
ノートブックをGit Hubに上げ直しました.

データセット準備

from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision import transforms
import numpy as np


BATCHSIZE = 128

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])

train_set = MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=BATCHSIZE, shuffle=True, num_workers=2)

test_set = MNIST(root='./data', train=False, download=True, transform=transform)
test_loader = DataLoader(test_set, batch_size=BATCHSIZE, shuffle=False, num_workers=2)

classes = tuple(np.linspace(0, 9, 10, dtype=np.uint8))

モデル定義

import torch
import torch.nn as nn
import torch.nn.functional as F

import optuna
optuna.logging.disable_default_handler()


#モデルの定義

#入力画像の高さと幅,畳み込み層のカーネルサイズ
in_height = 28
in_width = 28
kernel = 3
class Net(nn.Module):
  def __init__(self, trial, num_layer, mid_units, num_filters):
    super(Net, self).__init__()
    self.activation = get_activation(trial)
    #第1層
    self.convs = nn.ModuleList([nn.Conv2d(in_channels=1, out_channels=num_filters[0], kernel_size=3)])
    self.out_height = in_height - kernel +1
    self.out_width = in_width - kernel +1
    #第2層以降
    for i in range(1, num_layer):
      self.convs.append(nn.Conv2d(in_channels=num_filters[i-1], out_channels=num_filters[i], kernel_size=3))
      self.out_height = self.out_height - kernel + 1
      self.out_width = self.out_width - kernel +1
    #pooling層
    self.pool = nn.AvgPool2d(kernel_size=2, stride=2)
    self.out_height = int(self.out_height / 2)
    self.out_width = int(self.out_width / 2)
    #線形層
    self.out_feature = self.out_height * self.out_width * num_filters[num_layer - 1]
    self.fc1 = nn.Linear(in_features=self.out_feature, out_features=mid_units) 
    self.fc2 = nn.Linear(in_features=mid_units, out_features=10)

  def forward(self, x):
    for i, l in enumerate(self.convs):
      x = l(x)
      x = self.activation(x)
    x = self.pool(x)
    x = x.view(-1, self.out_feature)
    x = self.fc1(x)
    x = self.fc2(x)
    return F.log_softmax(x, dim=1)

畳み込み層はPyTorchのnn.ModuleList()を用いてリスト形式で定義します.

ここで,nn.Conv2d()は第1引数に「入力チャンネル数」を指定する必要があるのですが,第1層のみそれに「入力画像のチャンネル数」(今回はMNISTを用いたため1)を指定しなければならないため,あらかじめnn.ModuleList()を作る際に定義しておきます.
第2層以降は第1引数として「前層の出力チャンネル数」を指定すれば良いのでfor文より繰り返しリストに追加していきます.

また,各畳み込み層およびプーリング層を定義する際,それら出力の高さself.out_heightself.out_widthを計算させています.これは,全結合層nn.Linear()の定義時に「入力次元数」を指定しなければならないためです.全結合層の入力は1次元である必要があり(厳密にはバッチサイズも配慮するため2次元),直前の出力の高さself.out_feature,幅self.out_feature,チャンネル数num_filters[num_layer-1]をかけた値self.out_featureを求め,これをviewメソッドに与え特徴マップを整形します.

Train & Test

def train(model, device, train_loader, optimizer):
  model.train()
  for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()

def test(model, device, test_loader):
    model.eval()
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            pred = output.max(1, keepdim=True)[1]
            correct += pred.eq(target.view_as(pred)).sum().item()
    return 1 - correct / len(test_loader.dataset)

最適化手法の試行

import torch.optim as optim

def get_optimizer(trial, model):
  optimizer_names = ['Adam', 'MomentumSGD', 'rmsprop']
  optimizer_name = trial.suggest_categorical('optimizer', optimizer_names)
  weight_decay = trial.suggest_loguniform('weight_decay', 1e-10, 1e-3)
  if optimizer_name == optimizer_names[0]: 
    adam_lr = trial.suggest_loguniform('adam_lr', 1e-5, 1e-1)
    optimizer = optim.Adam(model.parameters(), lr=adam_lr, weight_decay=weight_decay)
  elif optimizer_name == optimizer_names[1]:
    momentum_sgd_lr = trial.suggest_loguniform('momentum_sgd_lr', 1e-5, 1e-1)
    optimizer = optim.SGD(model.parameters(), lr=momentum_sgd_lr, momentum=0.9, weight_decay=weight_decay)
  else:
    optimizer = optim.RMSprop(model.parameters())
  return optimizer

活性化関数の試行

def get_activation(trial):
    activation_names = ['ReLU', 'ELU']
    activation_name = trial.suggest_categorical('activation', activation_names)
    if activation_name == activation_names[0]:
        activation = F.relu
    else:
        activation = F.elu
    return activation

目的関数の定義

EPOCH = 10
def objective(trial):
  device = "cuda" if torch.cuda.is_available() else "cpu"

  #畳み込み層の数
  num_layer = trial.suggest_int('num_layer', 3, 7)

  #FC層のユニット数
  mid_units = int(trial.suggest_discrete_uniform("mid_units", 100, 500, 100))

  #各畳込み層のフィルタ数
  num_filters = [int(trial.suggest_discrete_uniform("num_filter_"+str(i), 16, 128, 16)) for i in range(num_layer)]

  model = Net(trial, num_layer, mid_units, num_filters).to(device)
  optimizer = get_optimizer(trial, model)

  for step in range(EPOCH):
    train(model, device, train_loader, optimizer)
    error_rate = test(model, device, test_loader)

  return error_rate

目的関数の定義ですが,Optunaは目的関数が小さくなるようにハイパーパラメータをチューニングするため,objective関数は認識率でなく誤り率error_rateを返すよう定義します.

パラメータチューニングの実行

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

実行結果

最適化されたハイパーパラメータの結果を出力します.

study.best_params

{'activation': 'ELU',
 'adam_lr': 0.00046155949650211135,
 'mid_units': 100.0,
 'num_filter_0': 128.0,
 'num_filter_1': 16.0,
 'num_filter_2': 112.0,
 'num_filter_3': 16.0,
 'num_filter_4': 128.0,
 'num_filter_5': 96.0,
 'num_filter_6': 128.0,
 'num_layer': 7,
 'optimizer': 'Adam',
 'weight_decay': 1.0303418654258521e-10}

また,最適化後の目的関数の値(誤り率)は,

study.best_value

0.00880000000000003

となりました.

チューニングを行わなかった場合との比較

今回,Optunaを使ってハイパーパラメータのチューニングを行いましたが,チューニングを行わなかった場合とどのような違いがあるのか,認識精度実行時間の観点から比較を行います.

チューニングを行わなかった場合のハイパーパラメータの値は以下のように指定しました.

  • 畳み込み層の数 : 3
  • 各畳み込み層のフィルタ数 : 16, 32, 48
  • 全結合層のユニット数 : 100
  • 活性化関数 : ReLU
  • 最適化手法 : Adam
  • 学習率 : 0.001
  • weight_decay : 0

認識精度と実行時間の比較

Optunaを用いてチューニングを行なった場合と行わなかった場合の認識精度および実行時間の比較です.

認識精度 実行時間(秒)
チューニングあり 0.9912 2580.886
チューニングなし 0.9881 58.232

認識精度においては,チューニングを行わなかった場合が0.9881であることに対し,チューニングした場合は0.9912となっており,精度が向上していることがわかります.わずかな差ではありますが,これはMNIST画像認識という簡単なタスクであったこともあり,複雑な問題の場合はより精度の向上が期待できると思います.

実行時間においては,当然ではありますがチューニングを行なったことによってより多くを要してしまいました.

まとめ

今回は,PyTorchによるMNISTの画像認識を通し,Optunaを用いてハイパーパラメータのチューニングを行なってみました.

特に,畳み込み層の数というネットワークの構造を大きく左右するパラメータをチューニングできたので個人的には満足です(PyTorchの場合,特徴マップの大きさを計算しなければならなかったのが若干面倒でしたが).

Optunaは複雑な問題であるほど効果を発揮できると思うので,機会が訪れ次第試してみたいと思います.

100
83
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
100
83