浮世絵作者予測

Open Review Competition

賞金: 150,000 参加チーム数: 151 終了まで: 62日

コンペティション概要

浮世絵の画像から、その画像の作者を識別しよう。
浮世絵のクラスは10種類あるため、多クラス分類モデルを作成します。

※なお、本コンペティションでは、学生賞を用意しております。
学生の方は、リーダーボード→LBチーム→リーダーボードチームリストより、「学生限定」のリーダーボードにご参加ください。

賞金

1位 100,000円
学生賞 50,000円

※ 対象者には、コンペティション終了後 メールにてご連絡いたします。

必要なスキル
  • Pythonの基本
  • 機械学習による多クラス分類

ダウンロード

データをダウンロードするにはログインまたはユーザー登録して下さい

概要

このデータセットは浮世絵のデータです。
米・メトロポリタン美術館及びニューヨーク公共図書館がCC0ライセンスで公開した両館のコレクションから浮世絵の画像を集めました。

各浮世絵の画像をデータは224×224の大きさでRGBの画像データです。

データ数は、訓練データが3158枚、テストデータが397枚です。

メトリック

このコンペティションは、accuracy(正解数/全サンプル数)によって評価されます。

ukiyoe-test-imgs.npzに対して、作成したモデルで予測を行い、 その結果を次のフォーマットのcsvファイルで提出してください。

提出ファイルのフォーマット

以下のフォーマットで提出します

id,y
1,0
2,5
3,2
...
■Open Review Competition

本コンペでは、開催期間終了後 賞金対象者のコードを公開し、ユーザーの皆様にチーティング有無をレビューしていただき順位確定させる、オープンレビュー方式のコンペティションを行います。

賞金対象ユーザー
コンペ終了後1週間以内:
トピックにて、学習過程の分かるコードの公開をお願いいたします。
(簡易解説までつけていただけると助かります。)
1.5か月以内:
レビュアー(ユーザー)より、チーティングの疑いに関するコメントがある場合は、ご回答をお願いいたします。
※チーティングとは無関係のコメント(ノウハウに関する質疑 等)についてもご回答いただけると幸いですが、順位確定の判断材料とは致しません。

レビュアー(ユーザーの皆様)
コンペ終了後1か月以内:
公開コードを確認いただき、チーティングが疑われる場合は、トピックを通して質疑の投稿をお願いいたします。

レビュアーからの質疑と、回答状況をふまえて、最終的に運営側で順位確定を判断します。

■順位の繰上げ

不正発覚の場合は失格とし、リーダーボードの順位を繰上げます。
順位繰上げにより賞金対象者となられた場合は、繰上げ日より一週間以内に、トピックにてコードを公開いただき、「Open Review Competition」と同様のフローにて順位を確定させていただきます。

■タイムライン

開始日 2019/10/2 0:00 JST
終了日 2020/1/14 0:00 JST
エントリー締め切り なし

■学生賞について

・コンペ終了までに、LBチームより「学生限定」リーダーボードにご参加ください。コンペ終了後の参加の場合は賞金対象外となります、ご注意ください。
・賞金のお支払いにあたり、本人確認のため学生証をご提示いただきます。

■システム利用

・参加者ごとに1つのアカウントでご参加ください
・チーム参加にの場合は、最大5名での参加が可能です
・1日あたり、最大5回までの提出が可能です

■禁止事項

・ユーザー間での情報共有
コンペティションに関連するコード・データを、チーム外のユーザーと共有することはできません。全参加者が利用できる場合に限り、共有可能です。
・外部データの使用
本コンペティションで公開されているデータのみを用いてチャレンジして下さい。コンペ外データを用いて学習されたモデルの使用も禁止とします。
※コンペ期間中、不正の疑われる結果が提出されている場合は、コード提出依頼や、データ削除対応させていただくことがございます。

■運営からのお願い

公平性の担保、チーティング等の不正防止のため、予告なくルールの追加・変更を行う場合がございます。
ご不便をおかけすることもあるかと思いますが、サービス向上のためご了承ください。

FAQ

このコンペティションでは賞金はでますか?

はい。最も精度の高い学習モデルを作成した優勝者には、賞金10万円を贈呈します。
順位確定までのプロセスについては、ルール「Open Review Competition」を参照ください。

チームで参加できますか?

可能です。チームページから作成いただけます。

どこでアカウントをつくればいいですか?

こちらから作成いただけます。

コンペティション参加にはアカウント登録が必要となりますのでご注意ください。

外部データを使うことは可能ですか?

本コンペティションで公開されているデータのみを用いてチャレンジして下さい。コンペ外データを用いて学習されたモデルの使用も禁止とします。

コードを提出するにあたって Seed を固定する必要はありますか?

Seed を固定することが推奨です.ただし,Seed を固定しなくても提出用コードとしては認めていく方針です。

概要

このチュートリアルでは, ukiyoeデータに対して

  • データの読み込み
  • データをプロットして確認
  • 前処理
  • kerasを用いてNNモデルの作成,学習
  • 誤識別したデータを確認

を行います.

環境

  • python 3.7.4
  • tensorflow 1.14.0
  • numpy 1.17.2
  • matplotlib 3.1.1

データのロード

まずはデータの読み込みをしてみましょう.

import numpy as np
import os

class UkiyoeDataLoader(object):
    """
    Example
    -------
    >>> ukiyoe_dl = UkiyoeDataLoader()
    >>> datapath = "./data"
    >>> train_imgs, train_lbls, validation_imgs, validation_lbls = ukiyoe_dl.load(datapath)
    """
    def __init__(self, validation_size: float):
        """
        validation_size : float
        [0., 1.]
        ratio of validation data
        """
        self._basename_list = [
        'ukiyoe-train-imgs.npz',\
        'ukiyoe-train-labels.npz'
        ]
        self.validation_size = validation_size

    def load(self, datapath: str, random_seed: int=13) -> np.ndarray:
        filenames_list = self._make_filenames(datapath)
        data_list = [np.load(filename)['arr_0'] for filename in filenames_list]

        all_imgs, all_lbls = data_list

        # shuffle data
        np.random.seed(random_seed)
        perm_idx = np.random.permutation(len(all_imgs))
        all_imgs = all_imgs[perm_idx]
        all_lbls = all_lbls[perm_idx]

        # split train and validation
        validation_num = int(len(all_lbls)*self.validation_size)

        validation_imgs = all_imgs[:validation_num]
        validation_lbls = all_lbls[:validation_num]

        train_imgs = all_imgs[validation_num:]
        train_lbls = all_lbls[validation_num:]

        return train_imgs, train_lbls, validation_imgs, validation_lbls

    def _make_filenames(self, datapath: str) -> list:
        filenames_list = [os.path.join(datapath, basename) for basename in self._basename_list]
        return filenames_list

データのフォーマットが.npzなので,numpynp.load関数を使って読み込みます.

それ以外のコードは,データを保存した場所(datapath)を渡すだけで,そこから読み込んでくれるようにするための処理です.

ここで定義したクラスを使うことで,以下のようにしてデータをロードすることができます.

datapath = "./data"
validation_size = 0.2
train_imgs, train_lbls, validation_imgs, validation_lbls = UkiyoeDataLoader(validation_size).load(datapath)

validation_sizeではテストデータの比率を指定しており,ここでは2割のデータをテストデートとして扱っています

プロットしてみよう

データを各クラスごとに,どんな画像データなのか表示してみます.
ここではプロットにmatplotlibを用います.

import numpy as np
import matplotlib.pyplot as plt

class RandomPlotter(object):
    def __init__(self):
        self.label_char = ["0", "1", "2", "3",\
                           "4", "5", "6", "7",\
                           "8", "9"]
        plt.rcParams['font.family'] = 'IPAPGothic'

    def _get_unique_labels(self, labels: np.ndarray) -> np.ndarray:
        label_unique = np.sort(np.unique(labels))
        return label_unique

    def _get_random_idx_list(self, labels: np.ndarray) -> list:
        label_unique = self._get_unique_labels(labels)

        random_idx_list = []
        for label in label_unique:
            label_indices = np.where(labels == label)[0]
            random_idx = np.random.choice(label_indices)
            random_idx_list.append(random_idx)

        return random_idx_list

    def plot(self, images: np.ndarray, labels: np.ndarray) -> None:
        """
        Parameters
        ----------
        images : np.ndarray
        train_imgs or validation_imgs

        labels : np.ndarray
        train_lbls or validation_lbls
        """
        random_idx_list = self._get_random_idx_list(labels)

        fig = plt.figure()
        for i, idx in enumerate(random_idx_list):
            ax = fig.add_subplot(2, 5, i+1)
            ax.tick_params(labelbottom=False, bottom=False)
            ax.tick_params(labelleft=False, left=False)
            img = images[idx]
            ax.imshow(img, cmap='gray')
            ax.set_title(self.label_char[i])
        fig.show()

このコードでは,各クラスについて一つずつランダムにデータを取り出して,それをプロットしています.

__init__()plt.rcParams['font.family'] = 'IPAPGothic'では,matplotlibのフォントを日本語に対応したものに変更しています.
もしもこのフォントがないとエラーが出る場合は,このフォントを入れるか,すでにある別の日本語対応フォントに変更してください.

_get_random_idx_list()では,各クラスごとにランダムにデータのインデックスを抜き出しています.

plot()内が実際に画像をプロットするコードで,matplotlibimshow()を用いて表示しています.

ここで定義したクラスを用いると,以下のようにしてデータをプロットすることができます.

RandomPlotter().plot(train_imgs, train_lbls)
RandomPlotter().plot(validation_imgs, validation_lbls)

以下のように出力を見ることで, どういった浮世絵データなのか確認できます.

plot.png
plot2.png

前処理

データの前処理を行います.

ここでは,画像データに対しては,

  • 数値データの型をfloat32へ変更
  • 値を[0, 255]から[0, 1]に標準化を行います.

ラベルデータに対しては,

  • 0から9intで表されたラベルを,one-hot表現に変更を行います.
import numpy as np
from tensorflow.keras.utils import to_categorical

class Preprocessor(object):
    def transform(self, imgs, lbls=None):
        imgs = self._convert_imgs_dtypes(imgs)
        imgs = self._normalize(imgs)
        if lbls is None:
            return imgs
        lbls = self._to_categorical_labels(lbls)
        return imgs, lbls

    def _convert_imgs_dtypes(self, imgs):
        _imgs = imgs.astype('float32')
        return _imgs

    def _normalize(self, imgs):
        _imgs = imgs / 255.0
        return _imgs

    def _to_categorical_labels(self, lbls):
        label_num = len(np.unique(lbls))
        _lbls = to_categorical(lbls, label_num)
        return _lbls

ここで定義したコードを用いると,以下のように前処理を行うことができます.

train_imgs, train_lbls = Preprocessor().transform(train_imgs, train_lbls)

識別してみよう

DNNのフレームワークであるkerasを用いて簡易なNNを作成して識別してみましょう.
kerasはtensorflowに統合され,tensorflowの高レベルAPIとなっているので,tensorflowからインポートします.
(tensorflow.kerasがないというエラーが出た場合は,古いバージョンのtensorflowを使用している可能性があるので,tensorflowをアップデートしてみてください.)

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.models import Sequential
from tensorflow.keras import layers
from tensorflow.keras import backend as K

from sklearn.metrics import log_loss


# dataの準備
datapath = ""
train_imgs, train_lbls, validation_imgs, validation_lbls = UkiyoeDataLoader(validation_size).load(datapath)

train_imgs, train_lbls = Preprocessor().transform(train_imgs, train_lbls)
validation_imgs, validation_lbls = Preprocessor().transform(validation_imgs, validation_lbls)

f = lambda a: np.histogram(a, bins=128)[0]
train_hists = np.apply_along_axis(f, 1, train_imgs.reshape(len(train_imgs), 224*224, 3))
train_hists = train_hists.reshape(len(train_imgs), -1).astype(np.float)
validation_hists = np.apply_along_axis(f, 1, validation_imgs.reshape(len(validation_imgs), 224*224, 3))
validation_hists = validation_hists.reshape(len(validation_imgs), -1).astype(np.float)

# modelの設定
batch_size = 128
label_num = 10
epochs = 4

# model作成
model = Sequential()

model.add(layers.Flatten())
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(label_num, activation='softmax'))

loss = keras.losses.categorical_crossentropy
optimizer = keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999)
model.compile(loss=loss, optimizer=optimizer, metrics=['accuracy'])

# modelを学習する
model.fit(train_hists, train_lbls,
    batch_size=batch_size,
    epochs=epochs,
    verbose=1,
    validation_data=(validation_hists, validation_lbls))

# modelを評価する
train_score = model.evaluate(train_hists, train_lbls)
y_train = model.predict(train_hists)
train_log_loss = log_loss(np.argmax(train_lbls, axis=1), y_train)
validation_score = model.evaluate(validation_hists, validation_lbls)
y_val = model.predict(validation_hists)
validation_log_loss = log_loss(np.argmax(validation_lbls, axis=1), y_val)
print('Train loss :', train_score[0])
print('Train accuracy :', train_score[1])
print('Train log loss :', train_log_loss)
print('validation loss :', validation_score[0])
print('validation accuracy :', validation_score[1])
print('validation log loss :', validation_log_loss)

画像のRGBごとのヒストグラムを入力とするdenseレイヤーを二段重ねたモデルです.

Train on 1613 samples, validate on 403 samples
Epoch 1/4
1613/1613 [==============================] - 0s 235us/sample - loss: 248.1627 - acc: 0.2728 - val_loss: 96.6572 - val_acc: 0.2953
Epoch 2/4
1613/1613 [==============================] - 0s 139us/sample - loss: 49.8124 - acc: 0.4048 - val_loss: 37.4576 - val_acc: 0.4094
Epoch 3/4
1613/1613 [==============================] - 0s 100us/sample - loss: 21.0337 - acc: 0.4761 - val_loss: 25.8304 - val_acc: 0.3970
Epoch 4/4
1613/1613 [==============================] - 0s 51us/sample - loss: 11.7665 - acc: 0.5189 - val_loss: 20.2862 - val_acc: 0.4293
1613/1613 [==============================] - 0s 53us/sample - loss: 8.3083 - acc: 0.5890
403/403 [==============================] - 0s 87us/sample - loss: 20.2862 - acc: 0.4293
Train loss : 8.308334161683927
Train accuracy : 0.58896464
Train log loss : 7.099104511534913
validation loss : 20.286180082089256
validation accuracy : 0.4292804
validation log loss : 12.11585940360455

出力を見てみると
このシンプルなモデルでは43から47%ほどのaccになるようです.
これをベースラインとして改善してみましょう.

識別結果の出力

学習したモデルにテストデータを入力し提出ファイルを作成します.

test_imgs = np.load('ukiyoe-test-imgs.npz')['arr_0']
test_imgs = Preprocessor().transform(test_imgs)
test_hists = np.apply_along_axis(f, 1, test_imgs.reshape(len(test_imgs), 224*224, 3))
test_hists = test_hists.reshape(len(test_imgs), -1).astype(np.float)
predict_lbls = model.predict(test_hists, batch_size=batch_size)
predict_lbls = np.argmax(predict_lbls, axis=1)

上のコードでは学習データのときと同様にデータの読み込みと前処理を行い, model.predict()を用いてテストデータに対する出力を得ています.

import pandas as pd

df = pd.DataFrame(predict_lbls, columns=['y'])
df.index.name = 'id'
df.index = df.index + 1
df.to_csv('predict.csv', float_format='%.5f')

最後に提出データのフォーマットに合わせるため, pandasnumpyのデータを渡し, インデックスとカラム名を付加します.
最後に少数表現でcsvファイルに書き出すことで提出ファイルが作成されます.

誤識別したデータの確認

そこで,誤識別した画像に限ってプロットしてみましょう.

class MisclassifiedDataPlotter(object):
    """
    このクラスへの入力はpreprocess処理済みのデータを仮定する.
    """
    def __init__(self):
        self.label_char = ["0", "1", "2", "3",\
                           "4", "5", "6", "7",\
                           "8", "9"]
        plt.rcParams['font.family'] = 'IPAPGothic'

    def _convert_onehot2intvec(self, labels):
        labels_int_vec = np.argmax(labels, axis=1)
        return labels_int_vec

    def _get_mixclassified_idx_list(self, labels_intvec, pred_labels_intvec):
        misclassified = labels_intvec != pred_labels_intvec
        mis_idxs_list = np.where(misclassified == True)[0]

        return mis_idxs_list

    def plot(self, images, labels, pred_labels, plot_num: int=5):
        """
        Parameters
        ----------
        images : np.ndarray
        train_imgs or validation_imgs

        labels : np.ndarray
        train_lbls or validation_lbls

        pred_labels : np.ndarray
        predicted labels by trained model

        plot_num : int 
        number of plot images
        """
        labels_intvec = self._convert_onehot2intvec(labels)
        pred_labels_intvec = self._convert_onehot2intvec(pred_labels)

        mis_idxs_list = self._get_mixclassified_idx_list(labels_intvec, pred_labels_intvec)
        random_idx_list = list(np.random.choice(mis_idxs_list, size=plot_num, replace=False))

        fig = plt.figure()
        for i, idx in enumerate(random_idx_list):
            ax = fig.add_subplot(1, plot_num, i+1)
            ax.tick_params(labelbottom=False, bottom=False)
            ax.tick_params(labelleft=False, left=False)
            img = images[idx].reshape((224, 224, 3))
            ax.imshow(img)

            actual_label = self.label_char[labels_intvec[idx]]
            pred_label = self.label_char[pred_labels_intvec[idx]]
            ax.set_title(f"{pred_label} : actual {actual_label}")
        fig.show()

このコードは,前処理をした後のデータを入力すると仮定しています.つまり,画像データは(224, 224, 3)float型を持つnp.ndarrayでラベルがone-hot表現になったデータです.

ここで定義したクラスを用いると,以下のようにして誤識別した画像を確認できます.

prediction = model.predict(validation_hists) # このmodelは上で作成したkerasのNNです.

mis_plotter = MisclassifiedDataPlotter()
mis_plotter.plot(validation_imgs, validation_lbls, prediction, plot_num=5)

以下が識別ミスをした画像になります.
クラス0に含まれる画像は青っぽい背景を持つものが多いようで, 青成分の多い画像はクラス0として識別されるようです.

misclassify.png