catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

ChainerでMTGのカードを分類する(識別フェーズ)

黄金週間なので趣味プログラミングに没頭です。かたりぃなです。

MTGのカードの種類を識別するPythonモジュールを作っていきたいと思います。

今回でPython側でchainerを使う部分はひとまず完成です。

環境

ソフトウェア

  • Windows10 Pro
  • VisualStudio 2015 community(C++用)
  • VisualStudio Code(Python用)
  • Python 3.5
  • Chainer 1.20.0.1

ハードウェア

Chainerで学習させる

前回Chainerをいじったとき、識別率が60%くらいで頭打ちになってしまいました。

http://catalina1344.hatenablog.jp/entry/2017/04/09/222330

せめてもう少しまともに識別できるようにしたいので、まずはここから。

AlexNetを真似してみる

alexnetを真似して、前回の学習モデルを少し改造してみました。 畳み込み+MaxPoolingの層が3層、線形結合が3層です。 最後の層の出力274が1-of-k符号化されたラベルそれぞれの確率です。 dropoutや正規化は今は入れていません。

class TEST_NN(chainer.Chain):
    def __init__(self):
        super(MLP, self).__init__(
            conv1 = F.Convolution2D(3, 96, 11, stride=4 ),
            conv2 = F.Convolution2D(None, 256, 5, stride=2 ),
            conv3 = F.Convolution2D(None, 384, 3, stride=1 ),
            l0=L.Linear(None, 1000),
            l1=L.Linear(None, 500),
            l2=L.Linear(None, 274),
        )

    def __call__(self, x):
        h = F.relu(self.conv1(x))
        h = F.max_pooling_2d(h, ksize=5, stride=2)

        h = F.relu(self.conv2(h))
        h = F.max_pooling_2d(h, ksize=3, stride=3)

        h = F.relu(self.conv3(h))
        h = F.max_pooling_2d(h, ksize=3, stride=3)

        h = self.l0(h)
        h = self.l1(h)
        h = self.l2(h)
        return h

学習用のデータを学習モデルに合わせる

MTGのカード画像は長方形なのですが、CNNの実装では正方形の画像を前提としていることが多いです。長方形画像を分類するNNを実装する方法がわからず仕舞いなので、入力画像自体を正方形にcropすることにしました。

前回作ったデータ水増し用のプログラムにcrop機能を追加します。OpenCV便利ですね。

void crop_transform(cv::Mat& src_img, cv::Mat& dst_img, int w, int h)
{
    auto type = src_img.type();
    auto step = src_img.step;
    cv::Mat img(w, h, type, src_img.data);
    dst_img = img.clone();
}

これでwとhに同じ値を与えれば正方形にcropできます。w=h=223としました。 こうして切り取ってノイズを付与して水増しすると次のようになります。

f:id:Catalina1344:20170504164419p:plain

よさげですね。 ちなみに、カードのテキスト部分にも分類するための情報は含まれていますが、イラストのほうが情報量が多いのでイラスト側を残すことにしました。

「わかったぞ!わかったぞ!わかっ・・・」みたいにフレーバーテキストだけで判別できるものもありますが、今は置いときます。

ちなみにMTGのカードって、ある時期からプレインズウォーカーだけカード名に頭がめり込んでいるような気がします。(最初気づいたのは「世界を目覚めさせるものニッサ」のとき)昔はカード名の部分は見切れてた気がするのですが。。。

学習結果を保存する

chainerのexamplesが出力するスナップショットは識別フェーズでは使わない情報がついています。(optimiser, updaterなど) 識別フェーズで必要なのは学習済みのモデルのデータ(重み係数とかバイアス項とか)なので、モデルだけ保存するコードを追加します。

    nn = TEST_NN()
    model = L.Classifier(nn)
    # 略

    # Run the training
    trainer.run()

    # モデルを保存する
    chainer.serializers.save_npz( os.path.join(args.out, "mymodel.npz"), nn)

Classifierの返すモデルでは損失関数を付与したモデルになってしまうので、その前でインスタンス化したネットワークをとっておき、学習終了時にこれをserializerで保存します。 これで生成されたモデルのnpzファイルは10MByteくらいになりました。

このエントリを書き終わってから気づきましたが、識別フェーズでもsoftmaxを通すので、別にClassifireしたモデル保存してもいいのかもしれません。後で考える。

学習の経過はこんなグラフになりました。

前回は60%前後の正答率で打ち止めだったので、この数字だけ見ると改善はしているようです。

過学習気味(学習用の水増しデータが似たようなものが多いから?)かもしれませんが、今の私には判断つかないので次に進みます。 f:id:Catalina1344:20170504164807p:plain f:id:Catalina1344:20170504164814p:plain

実行にかかる時間は、ミニバッチサイズ100として20epoch回すのに30分程度です。 SSDの容量が厳しくなってきたのでHDD上に画像データを置いて実験しましたがお話になりませんでした。。

学習済みモデルをPythonで読み込んで使う

コマンドラインから実行する実験コードを書きました。

-iオプションで与えた画像ファイルを読み込んで、それをNNに入力してから結果を観測するだけのものです。

import training         # 自前の学習器
import chainer.serializers
import chainer.functions as F
import argparse
import numpy as np
from PIL import Image

def main():
    parser = argparse.ArgumentParser(description='TEST_NN')
    parser.add_argument('--inputimage', '-i', action='store', default=100,
                        help='image file name')
    args = parser.parse_args()
    filename = args.inputimage
    print("{}".format(filename) )

    # 学習済みモデルを利用する準備
    model = training.TEST_NN()
    chainer.serializers.load_npz("result\mymodel.npz", model)

    # テスト用画像を読み込む
    image = Image.open(filename)

    # Chainerのモデルに入力できるように、データの順序を合わせてNumpy形式の4次元テンソルにする
    pixels = np.asarray(image).astype(np.float32)
    pixels = pixels.transpose(2, 0, 1)
    pixels = pixels.reshape((1,) + pixels.shape)

    # 識別
    y = model(pixels)
    prediction = F.softmax(y)
    m = np.argmax(prediction.data)
    print("result={}".format(m) )

if __name__ == '__main__':
    main()

学習に使ったデータとかを適当に突っ込んでみます。

> python .\detect.py -i "dataset/185/185_48.png"
dataset/185/185_48.png
result=185
> python .\detect.py -i "dataset/121/121_3.png"
output/121/121_3.png
result=121

とりあえず分類はできているようです。

ピクセル列を4次元テンソルに変換している部分はchainerのdatasetあたりのコードから引っ張ってきたもので、理屈はまだよくわかっていません。(特に最初の次元は何を表しているものなのか。。。)

感想

これでMTGのカードのうち「カラデシュ」は分類できるようになりました。

コマンドラインから叩くだけでは感動は少なめですが、ARアプリのための土台ができつつあります。

「274種類のイラストを分類して任意のモデルを表示できる」という部分だけを誇大解釈すればVuforiaを超えたかもしれません。(超えてない)

今後の課題

  • 分類ラベルを増やすとどうなるか
  • 実際の映像からの分類でどれくらいの精度が出るか

などがあると思います。 前者は学習時間がかかるので、夜間とかに実行しておくなど時間を有効活用して研究したいと思います。

MTGはカード種類が多いですが、所詮は人工物なので階層的な分類が効果的なのかもしれないと思っています。(エキスパンションを識別してから、特定エキスパンション内で分類するなど)

後者はHoloLensと連携できるようにしてから改善していく予定です。 実際にモノを動かしながらやったほうが楽しいですし。

次の予定

これでカードイラストからカードを分類する仕組みをPython上で実現できました。 次はいよいよHoloLensとの連携部分です。 データの収集と分析といえばfluentdですが、HoloLens上に載せるには少々敷居が高そうです。

HoloLensからChainerを走らせているPCへHTTP/PUTして、HoloLens側は分類結果を受け取るのがいいかなと思っています。

それでは今回はこれくらいで。