104
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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_heightと__幅__self.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は複雑な問題であるほど効果を発揮できると思うので,機会が訪れ次第試してみたいと思います.

104
84
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
104
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?