[solafune] 衛星画像から空港利用者数予測ベースモデル with Keras

公開:2020/11/03
更新:2020/11/04
13 min読了の目安(約12500字TECH技術記事

はじめに

こんにちは。belugaです。普段は工場の異常検知などのAI作成に取り組んでいます。

以前、mygleさんがPyTorchでResNetで衛星コンペのベースモデルを公開していたので、今回はKerasでベースモデルを紹介します。

solafuneの衛星画像コンペのベースラインモデルの紹介です。フレームワークはTensorflowの高級APIのKerasで使用モデルはVGG16で、転移学習を行います。

一旦動かすことが大事だと思うのでなるべくシンプルにしてます。

流れとしては、①モデル作成し、②データセットを作成、③学習、④推論という4本立てです。コードだけ追っていくと何をしているかわからないと思うので、基本はこの4つを意識してください。

最後に1つのファイルにまとめてるので、時間のない方は最後のファイル部分をコピペして学習に利用してください。

TensorflowのバージョンはGPU付き(tensorflow-gpu)で2.3.1で動作させてます。その他はOpenCV(opencv-python)のバージョン 4.4と、numpy, pandas, tqdm, scikit-learnをpip installすれば動作します。Google Colabなどなら確実に動作するかと思います。環境構築の際のDockerファイルは最後に記載していますが、ライブラリ足りなければ各自インストールお願いします。

ディレクトリ構成

パスなどのこともあるので最初に書いておきます。
直下にcsvファイル群とdataがあります。

> ├ data
>   ├ trainimage
>     ├ image
>   ├ testimage
>     ├ image
>   ├ evaluatemodel
>     ├ image
> ├ testdataset_anotated.csv # 学習
> ├ traindataset_anotated.csv # 評価
> ├ uploadfile.csv # アップロード

①モデル作成

15層までをフリーズし、16層以降を学習にしてます。

その後全結合層を2層追加し、最後は活性化関数のlinearを適用し、回帰問題として扱っています。CNNを利用した深層学習では一般的には分類・物体検出・セグメンテーションなどが取り上げられると思うのですが、このように回帰問題として扱ったことはなかったので新鮮です。回帰問題とするので、linearの活性化関数を使用していますが、reluでも問題ないと思います。

またLOSSはターゲットの旅行客の生の数値を扱う場合、logで変換するものにしないと、やばいぐらいでかい数値がでるので注意してください。自分もはじめやらかしました。

ターゲットの値を対数変換などしてからだと、MSEなどのLOSSを使用しても問題ないと思います。今回は、Kerasで用意されているmean_squared_logarithmic_errorをLOSSで使用してます。

学習率の初期値ですが0.00001(1e-5)スタートでないと発散します。

def vgg16(imgsize):
    
    input_tensor = (imgsize,imgsize, 3)
    base_model_max = VGG16(input_shape=input_tensor, weights='imagenet', include_top=False, pooling='max')

    for layer in base_model_max.layers[:15]:
        # from 1st to 15th freeze
        layer.trainable = False
    
    for layer in base_model_max.layers[15:]:
        # from 16th activate
        layer.trainable = True

    model = Sequential()
    model.add(base_model_max)
    model.add(Flatten())

    model.add(Dense(512)) # 全結合層
    model.add(Activation('relu'))
    model.add(Dropout(0.5))

    model.add(Dense(64)) # 全結合層
    model.add(Activation('linear'))
    model.add(Dropout(0.5))

    model.add(Dense(1)) # 出力層
    model.compile(loss='mean_squared_logarithmic_error', optimizer=tf.keras.optimizers.RMSprop(lr=1e-5))

    return model

②データセット作成

ここがKerasのImageDataGeneratorのややこしい部分なのですが、データ取得方法はKerasでは3種類存在し、flow, flow_from_directory, flow_from_dataframeです。使い分けとしてはnumpyなどで既にデータのインスタンスが存在してる場合はflowを使用し、実際MNISTのチュートリアルではほとんどflowを使用してるかと思います。
flow_from_directoryの場合、ラベル分けされたデータがディレクトリに保存されてるときに使用します。
最後のflow_from_dataframeが今回使用しているタイプでcsvなどに正解ラベルや今回のように正解の数値データが記載されているときに使用します。また実装でやや詰まった部分としてはclass_mode="raw" であり、ターゲットの値をそのまま扱えるようです。あまり画像で回帰など扱わないので、やや詰まりました。

今回は、データ拡張ありで学習してます。KerasのImageDataGeneratorではPyTorchで使用できるOSSのAlbumentationと比較すると少ないです。もしデータ拡張を追加したい場合は、自分でデータジェネレータを作成して、データ拡張を実装するか、AlbumentationをKerasでも実行できるように実装してください。

def get_datagen(mode, df, target_size, batch_size):
    if mode == 'train':
        train_datagen = ImageDataGenerator(
            rescale=1./255,
            rotation_range=15,
            shear_range=0.2,
            horizontal_flip=True,
            width_shift_range=0.1,
            height_shift_range=0.1
        )

        train_datagenerator = train_datagen.flow_from_dataframe(
            df,
            "data/trainimage/image",
            x_col='image',
            y_col='traveler',
            target_size=target_size,
            class_mode="raw", # for regression
            batch_size=batch_size,
            seed=42
        )
    
        return train_datagenerator

    elif mode == 'valid':
        valid_datagen = ImageDataGenerator(rescale=1./255)
        valid_datagenerator = valid_datagen.flow_from_dataframe(
            df,
            "data/testimage/image",
            x_col='image',
            y_col='traveler',
            target_size=target_size,
            class_mode="raw", # for regression
            batch_size=batch_size,
            seed=42
        )

        return valid_datagenerator

③学習

学習体制は下記のような感じです。早期終了を追加してます。通常早期終了の場合、学習がストップしたときのLOSSの値は悪いので、各自LOSSが一番良かったときのmodelを保存かつ、ロードするようにしておいてください。

また、historyを図示して保存などコードの可読性が下がるため今回のtutorialでは実装してないので、各自実装してください。

if __name__ == "__main__":

    # csv読み込み
    train = pd.read_csv('traindataset_anotated.csv', names=["image","traveler"]) # headerあり読み込み
    valid = pd.read_csv('testdataset_anotated.csv', names=["image","traveler"]) # headerあり読み込み

    # ハイパーパラメータ
    batch_size=10
    epochs = 200
    imgsize = 224
    target_size = (imgsize, imgsize)

    # データセット取得
    train_datagenerator = get_datagen("train", train, target_size, batch_size)
    valid_datagenerator = get_datagen("valid", valid, target_size, batch_size)
    
    # 早期終了
    early_stop = EarlyStopping(monitor='val_loss', patience=7, verbose=1, mode='auto') # val_loss
    
    # モデル読み込み
    model = vgg16(imgsize)

    # 学習
    history = model.fit(train_datagenerator,
                    epochs=epochs,
                    validation_data=valid_datagenerator,
                    verbose=1,
                    shuffle=True,
                    callbacks=[early_stop])
  # モデル保存
    model.save("solafune_tutorial.h5")

④推論

画像の前処理で、学習時の画像サイズ224の正方形に揃えています。
今回のチュートリアルでは200エポックで学習してますが、200エポックでも学習終了しませんでした。

    # 推論
    upload = pd.read_csv('uploadfile.csv', names=["image","traveler"]) # headerあり読み込み
    evaluate_iter = pathlib.Path('data/evaluatemodel/image').glob('*.jpg')
    
    for evaluate_path in tqdm(evaluate_iter):
        _path = str(evaluate_path)
        _path = _path.split('/')[-1]

        # 推論前処理部分
        evaluate_img = cv2.imread(str(evaluate_path))
        evaluate_img =  cv2.cvtColor(evaluate_img, cv2.COLOR_BGR2RGB)
        evaluate_img =  cv2.resize(evaluate_img, (imgsize, imgsize))
        evaluate_img = np.array(evaluate_img / 255.)
        evaluate_img = evaluate_img.reshape(1, imgsize, imgsize, 3)
    
        predict_num = int(model.predict(evaluate_img))
        upload.loc[upload['image'] == _path, 'traveler'] = int(predict_num)

    upload.to_csv("submit_solafune_tutorial.csv", header=False, index=False)

提出

どきどきのアップロードです。

結果は、、、

結果としては、2.64でいい感じの値が出ましたね!

今後のモデルの拡張について

こういうチュートリアルだと、やってみましたで終わりだと思うので、今後こうした方がいいのではということを書いておきます。

ターゲットの対数変換

まずはこれが考えられます。統計学では歪度というデータの偏りを算出する方法があるのですが、これが偏っている場合、なるべく歪度を-1~1以内の正規分布にしましょうというアプローチが考えられます。

3.39の歪度が対数変換することで0.80まで減少してるかと思います。

train = pd.read_csv('traindataset_anotated.csv', names=["image","traveler"]) # headerあり読み込み
train_x, train_y = train.iloc[:,:1], train.iloc[:,1:2] 

print('travelers:', skew(train_y)) => travelers: [3.39508326]
print('log travelers:', skew(np.log1p(train_y))) => log travelers: [0.80800331]

データ拡張

雲がある画像など、色がハイライトされている画像が多いのでここらへんをデータ拡張に入れてみてはどうでしょうか。

学習モデル

学習データ少ないので、もしかしたらVGG16は有効でないかもしれません。他のモデルも検討してみましょう。

おわりに

あまりコンペには参加したことが無かったので、色々勉強になりました。
Kaggleの場合、NoteBook写経→わからねというムーブをかましがちだったのですが、一からコード書いたので実装面で成長した気がします。

今後運営側に期待することは、チームマージできることや、学習データをもっと増やしていただけたらと思います。Twitterでのリプライの速さは尊敬してます。

また、これ見てる皆さんリプライお願いします!

環境構築

Dockerファイルを作成し、イメージをビルドして使用してください。もしくは、GoogleColabやAnacondaで以下のライブラリのインストールをしてください。

以下はDockerfileです。

FROM tensorflow/tensorflow:2.3.1-gpu-jupyter

RUN apt-get update
RUN apt-get install -y libgl1-mesa-dev
RUN pip install pandas
RUN pip install tqdm
RUN pip install opencv-python
RUN pip install tensorflow-addons
RUN pip install scikit-learn
RUN apt-get update && apt-get install -y git

一連の学習をまとめたもの

下記のファイルをコピペして利用すればVGG16で学習できます。ディレクトリ構成には注意してください。

import sys
import os
import pathlib
import glob
import math
import random
import argparse
import time

import cv2
from tqdm import tqdm
import pandas as pd
import numpy as np

from tensorflow.keras.optimizers import SGD
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalization
from tensorflow.keras.layers import Convolution2D, MaxPooling2D, GlobalMaxPooling2D, Concatenate
from tensorflow.keras.applications.vgg16 import VGG16
import tensorflow as tf
from tensorflow.keras.layers import Input
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import EarlyStopping, LearningRateScheduler, Callback, ModelCheckpoint
import tensorflow as tf


def vgg16(imgsize):
    
    input_tensor = (imgsize,imgsize, 3)
    base_model_max = VGG16(input_shape=input_tensor, weights='imagenet', include_top=False, pooling='max')

    for layer in base_model_max.layers[:15]:
        # from 1st to 15th freeze
        layer.trainable = False
    
    for layer in base_model_max.layers[15:]:
        # from 16th activate
        layer.trainable = True

    model = Sequential()
    model.add(base_model_max)
    model.add(Flatten())

    model.add(Dense(512)) # 全結合層
    model.add(Activation('relu'))
    model.add(Dropout(0.5))

    model.add(Dense(64)) # 全結合層
    model.add(Activation('linear'))
    model.add(Dropout(0.5))

    model.add(Dense(1)) # 出力層
    model.compile(loss='mean_squared_logarithmic_error', optimizer=tf.keras.optimizers.RMSprop(lr=1e-5))

    return model

def get_datagen(mode, df, target_size, batch_size):
    if mode == 'train':
        train_datagen = ImageDataGenerator(
            rescale=1./255,
            rotation_range=15,
            shear_range=0.2,
            horizontal_flip=True,
            width_shift_range=0.1,
            height_shift_range=0.1
        )

        train_datagenerator = train_datagen.flow_from_dataframe(
            df,
            "data/trainimage/image",
            x_col='image',
            y_col='traveler',
            target_size=target_size,
            class_mode="raw", # for regression
            batch_size=batch_size,
            seed=42
        )
    
        return train_datagenerator

    elif mode == 'valid':
        valid_datagen = ImageDataGenerator(rescale=1./255)
        valid_datagenerator = valid_datagen.flow_from_dataframe(
            df,
            "data/testimage/image",
            x_col='image',
            y_col='traveler',
            target_size=target_size,
            class_mode="raw", # for regression
            batch_size=batch_size,
            seed=42
        )

        return valid_datagenerator


if __name__ == "__main__":

    # csv読み込み
    train = pd.read_csv('traindataset_anotated.csv', names=["image","traveler"]) # headerあり読み込み
    valid = pd.read_csv('testdataset_anotated.csv', names=["image","traveler"]) # headerあり読み込み

    # ハイパーパラメータ
    batch_size=10
    epochs = 200
    imgsize = 224
    target_size = (imgsize, imgsize)

    # データセット取得
    train_datagenerator = get_datagen("train", train, target_size, batch_size)
    valid_datagenerator = get_datagen("valid", valid, target_size, batch_size)
    
    # 早期終了
    early_stop = EarlyStopping(monitor='val_loss', patience=7, verbose=1, mode='auto') # val_loss
    
    # モデル読み込み
    model = vgg16(imgsize)

    # 学習
    history = model.fit(train_datagenerator,
                    epochs=epochs,
                    validation_data=valid_datagenerator,
                    verbose=1,
                    shuffle=True,
                    callbacks=[early_stop])


    # モデル保存
    model.save("solafune_tutorial.h5")

    # 推論
    upload = pd.read_csv('uploadfile.csv', names=["image","traveler"]) # headerあり読み込み
    evaluate_iter = pathlib.Path('data/evaluatemodel/image').glob('*.jpg')
    
    for evaluate_path in tqdm(evaluate_iter):
        _path = str(evaluate_path)
        _path = _path.split('/')[-1]

        # 推論前処理部分
        evaluate_img = cv2.imread(str(evaluate_path))
        evaluate_img =  cv2.cvtColor(evaluate_img, cv2.COLOR_BGR2RGB)
        evaluate_img =  cv2.resize(evaluate_img, (imgsize, imgsize))
        evaluate_img = np.array(evaluate_img / 255.)
        evaluate_img = evaluate_img.reshape(1, imgsize, imgsize, 3)

        predict_num = int(model.predict(evaluate_img))
        upload.loc[upload['image'] == _path, 'traveler'] = int(predict_num)

    upload.to_csv("submit_solafune_tutorial.csv", header=False, index=False)