見出し画像

自前画像によるkerasのハイパーパラメータ調整(hyperas編)

(noteでがっつりコード書くのってどうなんだ...?)

学習モデルを作ってると、ハイパーパラメータ調整したいことがよくあります。

「どこかにハイパーパラメータ調整できるツール落ちてないかなあ」

と思って探すわけなのですが、あるじゃないですか!

というわけで今回はkerasでハイパーパラメーターを調整できるツールである"hyperas"を使って、自前で準備した画像を認識してくれるモデルを作ります。MNISTは使いません。hyperasのgithubはこちら

使うデータはKaggleにあるDogs vs Catsのデータです。
ここからダウンロードできます。
以下と同じようにフォルダに格納出来れば何のデータでもいいです。
一応全部使ってしまうとパラメータ調整するまでもなくいい精度が出てしまいそうだったので、trainを犬猫各2000枚、testを各500枚まで減らしました。

hyperasのインストール

pip install hyperas

ファイル構成

下記のような形で画像が格納されているとします。


コード

とりあえず必要なものをimportします。

from keras.applications.resnet50 import ResNet50
from keras.applications.vgg16 import VGG16
from keras.models import Model
from keras.layers import Dense, Dropout, Input, GlobalAveragePooling2D
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import ModelCheckpoint, CSVLogger, ReduceLROnPlateau
from keras.optimizers import SGD
import numpy as np
import keras.backend as K
import glob
import os
import cv2

from hyperopt import Trials, STATUS_OK, tpe
from hyperas import optim
from hyperas.distributions import choice, uniform

下の3つがhyperasに関わるものです。とりあえずimportしておきます。
hyperasでは大きく2つの関数を作ってハイパーパラメータ調整を行います。その2つとは
関数①:入力データを作る関数
関数②:データを受け取って、実際にモデルを学習する関数
です。この時①の返り値と②の引数には同じものを同じ順番で指定する必要があります。

関数①について

def data():
    img_width = 224
    img_height = 224
    x_train = []
    y_train = []
    x_val = []
    y_val = []
    # 以下、フォルダ内の画像を読み込む
    train_Folders = glob.glob(os.path.join(os.getcwd(), "data", "train", "*"))
    test_Folders = glob.glob(os.path.join(os.getcwd(), "data", "val", "*"))
    
    # 訓練用画像読み込み
    for folder in train_Folders:
        temp_files = glob.glob(os.path.join(folder, "*"))
        for file in temp_files:
            image = cv2.imread(file)
            image = cv2.resize(image, (img_width, img_height))
            x_train.append(image)
            if os.path.basename(folder) == "cats":
                y_train.append(0)
            else:
                y_train.append(1)

    # 検証用画像読み込み
    for folder in test_Folders:
        temp_files = glob.glob(os.path.join(folder, "*"))
        for file in temp_files:
            image = cv2.imread(file)
            image = cv2.resize(image, (img_width, img_height))
            x_val.append(image)
            if os.path.basename(folder) == "cats":
                y_val.append(0)
            else:
                y_val.append(1)

    x_train = np.array(x_train)
    y_train = np.array(y_train)
    x_val = np.array(x_val)
    y_val = np.array(y_val)
    return x_train, y_train, x_val, y_val

flow_from_directory()せず愚直に読み込みます。愚直に読み込んでるだけです。もっといい書き方があれば教えてください。

関数②について

def create_trained_model(x_train, y_train, x_val, y_val):
    batchSize = 10
    img_width = 224
    img_height = 224
    train_datagen = ImageDataGenerator(rescale=1.0 / 255)
    test_datagen = ImageDataGenerator(rescale=1.0 / 255)

    # ここからモデル作成
    input_tensor = Input(shape=(img_width, img_height, 3))
    select_base = {{choice(['resnet50', 'densenet121'])}}
    if select_base == 'resnet50':
        base_model = ResNet50(weights='imagenet', include_top=False, input_tensor=input_tensor)
    else:
        base_model = VGG16(weights='imagenet', include_top=False, input_tensor=input_tensor)
    x = base_model.output
    x = GlobalAveragePooling2D()(x)
    x = Dense({{choice([512, 1024, 2048])}}, activation="relu")(x)
    x = Dropout({{uniform(0, 1)}})(x)
    predictions = Dense(1, activation="sigmoid")(x)
    model = Model(inputs=base_model.input, outputs=predictions)

    opt = SGD(lr=0.001, momentum=0.9)  # Adam()
    model.compile(optimizer=opt, loss='binary_crossentropy', metrics=['accuracy'])
    
    # モデルを保存したり、学習率を調整したり。
    checkpointer = ModelCheckpoint(
        filepath='./checkpoints/weights_epoch{epoch:02d}-{val_loss:.2f}.hdf5',
        save_best_only=True
    )
    csv_logger = CSVLogger('model.log')
    reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=0.00001)
    
    # 実際に学習させる
    history = model.fit_generator(
        train_datagen.flow(x_train, y_train, batch_size=batchSize),
        steps_per_epoch=len(x_train) // batchSize,
        epochs=5,
        verbose=1,
        validation_data=test_datagen.flow(x_val, y_val),
        validation_steps=len(x_val) // batchSize,
        callbacks=[reduce_lr, csv_logger, checkpointer]
    )

    val_loss, val_acc = model.evaluate(x_val, y_val)
    return {'loss': val_loss, 'status': STATUS_OK, 'model': model}

今回はファインチューニングすることにします。
注目すべきはコードの中盤から出現する

{{choice([xxx, xxx])}}

という表現です。hyperasではchoiceすることで[ xxx, xxx ]内から1つ選択して学習を行うようになります。今回はファインチューニングの元になるモデルをVGG16とResNet50から選択し、さらに中間層を大きさを3択から調整することにしました。
また、Dropout層で書かれている

{{uniform(0, 1)}}

では、ドロップアウトの割合をuniform(一様分布)の中からランダムで1つ選んでくれます。それ以外のモデルの書き方は普段と一緒です。この関数内でモデルを定義して、コンパイルして、実際に学習するところまでを行います。

メイン関数

best_run, best_model = optim.minimize(model=create_trained_model,
                                      data=data,
                                      algo=tpe.suggest,
                                      max_evals=8,
                                      trials=Trials()
                                      )
X_train, Y_train, X_val, Y_val = data()
print("Evalutation of best performing model:")
print(best_model.evaluate(X_val, Y_val))
print("Best performing model chosen hyper-parameters:")
print(best_run)

optim.minimizeという関数の中で実行しています。algoとtrialsはよくわかりません...。
max_evalsが調整回数です。今回はベースとなるモデル(2種類)、隠れ層1つ(3種類)、ドロップアウト割合(一様分布)なので、24回くらいは調整すべきかなと思います。
計算が終わると、結果が以下のように返されます。

Best performing model chosen hyper-parameters:
{'Dense': 0, 'Dropout': 0.7628124303271064, 'select_base': 1}

今回でいうと、
中間層:512
ドロップアウト割合:0.76
ファインチューニングの元になるモデル:densenet121
がいいと分かりました。

気になった点

・実行時にはimport部分と関数①、②だけを取り出したtemp_model.pyが同じディレクトリ内に作られ、それを実行するような挙動を取るようです。つまり、それ以外の場所に書かれているものはtemp_modelに読み込まれないため、必要な引数やコードは全て関数内に書き込むか、もしくは外においてimportする必要がありそうです。
・flow_from_directory()を使うと、x_trainのようなデータを準備することが出来ません。その場合、関数①はダミー関数として何も返り値を与えず、関数②の方ですべての操作を完結できるようにコーディングしてあげると動きます。関数①が返り値なしなので、関数②の引数も何もなしです。
・噂によるとコメントアウトした場所についてもchoiceが走るらしいです。

この記事が気に入ったらサポートをしてみませんか?