旅行好きなソフトエンジニアの備忘録

プログラミングや技術関連のメモを始めました

【Keras】EfficientNetのファインチューニング例

EfficientNetはAutoMLで作成された、パラメータ数の少なさに対して精度が非常に高いモデルです。 OfficialのTensorflowの実装だけでなく、PyTorchやKerasの実装も早速公開されており、使い方を知っておきたく試してみました。

実施内容

EfficientNetをファインチューニングして犬・猫分類を実施してみる

EfficientNet利用手順

① 以下のKeras版実装を利用しました。準備は"pip install -U efficientnet"を実行するだけです。
注意点としては、Kerasのバージョンが2.2.0以上であることが指定されています。
始め2.1.5で試したところ、"DepthwiseConvolutionが無い"といったエラーが出ました。
github.com
② あとは実装ですが、まずプロジェクトフォルダが以下の構成になっていることが前提です。

  • train
    • cats(このフォルダに学習用猫画像が入っている)
    • dogs(このフォルダに学習用犬画像が入っている)
  • val
    • cats(このフォルダに検証用猫画像が入っている)
    • dogs(このフォルダに検証用犬画像が入っている)
  • adam_mult_lr.py(ファインチューニング用にカスタマイズされたAdamオプティマイザ)
  • data_loader.py(fit_generatorに使うgenerator生成クラス)
  • train.py(学習実行ファイル)


まずadam_mult_lr.pyですが、ここでは学習済みEfficientNetを使ってファインチューニングしようとしています。
EfficientNetの特徴抽出部のレイヤーの学習係数を小さくしたいのですが、Kerasだと簡単には出来なさそうです。 ただ幸いこのAdamを使えばレイヤー毎に学習係数を変更できるよ、というブログを見つけたのでそれをコピーしてます。

erikbrorson.github.io

次にdata_loader.pyですが、以前の記事に書いた雛形ほぼそのものになります。
注意点としては、Keras版EfficientNetは画像がRGBであることを期待しているっぽく、 opencvを使った場合はBGRをRGBに変換するロジック追加が必要です。

ni4muraano.hatenablog.com

最後にtrain.pyは以下になります。見るとわかるのですが、ソースはかなり単純です。

from efficientnet import EfficientNetB0
from keras.layers import GlobalAveragePooling2D, Dense, Dropout
from keras.models import Model
from data_loader import DataLoader
from adam_lr_mult import Adam_lr_mult

def prepare_new_model(input_shape, class_count):
    # 学習済みモデルの取り出し
    feature_extractor = EfficientNetB0(input_shape=input_shape, weights='imagenet', include_top=False)
    # 犬猫分類器を引っ付ける
    x = feature_extractor.output
    x = GlobalAveragePooling2D()(x)
    x = Dense(512, activation='relu')(x)
    x = Dropout(rate=0.25)(x)
    x = Dense(class_count, activation='sigmoid')(x)
    # 新たなモデルの定義
    model = Model(inputs=feature_extractor.input, outputs=x)
    print(model.summary())
    return model

def get_adam_for_fine_tuning(lr, decay, multiplier, model):
    lr_multiplier = {}
    # 自分が引っ付けたレイヤーの学習係数は1、学習済みの部分は小さな値を設定する
    for layer in model.layers:
        if 'dense' in layer.name:
            lr_multiplier[layer.name] = 1.0
        else:
            lr_multiplier[layer.name] = multiplier
    return Adam_lr_mult(lr=lr, decay=decay, multipliers=lr_multiplier)

def train(epochs, batch_size, input_shape, class_count):
    # 学習用画像データローダー
    train_data_loader = DataLoader('train', batch_size, input_shape, do_augmentation=True)
    train_generator = train_data_loader.get_data_loader()
    # 検証用画像データローダー
    val_data_loader = DataLoader('val', batch_size, input_shape, do_augmentation=False)
    val_generator = val_data_loader.get_data_loader()
    # モデルの生成
    model = prepare_new_model(input_shape, class_count)
    # ファインチューニング用Adamオプティマイザ
    optimizer = get_adam_for_fine_tuning(lr=1e-3, decay=1e-5, multiplier=0.01, model=model)
    # コンパイルして
    model.compile(optimizer, loss='categorical_crossentropy', metrics=['accuracy'])
    # fit_generatorするだけ
    h = model.fit_generator(train_generator, train_data_loader.iterations, epochs,
                                  validation_data=val_generator, validation_steps=val_data_loader.iterations)
    # 学習データ、検証データのロスとAccをファイルに出力
    with open('loss.csv', 'a') as f:
        for loss_t, acc_t, loss_v, acc_v in zip(h.history['loss'], h.history['acc'], h.history['val_loss'], h.history['val_acc']):
            f.write(str(loss_t) + ',' + str(acc_t) + ',' + str(loss_v) + ',' + str(acc_v) + '\n')


if __name__ == '__main__':
    epochs = 5
    batch_size = 16
    input_shape = (224, 224, 3)
    class_count = 2
    train(epochs, batch_size, input_shape, class_count)


以下は5エポック学習した時のAccの推移ですが、こんな適当に作った物でもかなり良い結果が得られています。 f:id:ni4muraano:20190616083143p:plain

試しにResnet50でも同様の実験を行いました。論文上ではEfficientNetの方が僅かに精度が高いのですが、5エポック試した範囲ではResnet50の方が僅かに精度が高い結果となりました。 f:id:ni4muraano:20190616083256p:plain