« 百均のiPhoneケースもアイデア次第で豪華に!? | トップページ | Raspberry Pi Zero向け小型カメラモジュール »

2017年3月13日 (月)

TensorFlowで「けものフレンズ」の”フレンズ判別器”作ってみた

先日の記事(OpenCVによる”アニメ顔”検出)はこの伏線だったんです。

子供向けのような絵柄なのに、出てくるキャラがあまりにもIQ低め能天気なのに、背景にある世界観があまりにもシビアで意味深すぎて話題の「けものフレンズ」。

私もすっかりはまってます。サーバルちゃんが可愛すぎて死にそう、毎週楽しみで夜も眠れません。

ところがだんだんと登場人物・・・いやフレンズが増えてきたため、そろそろどの顔が何のフレンズさんかわからなくなってきた今日この頃。

こういう悩みにこそ”人工知能”の出番

てことで、TensorFlowを使って「けものフレンズ」の顔の画像を読み込むと何のフレンズかを答えてくれる判別器を作ってみました。

さて、それじゃあ今まで使ったTensorFlowのプログラムコードを使って…といきたいところですが。

実は今回使い勝手を大幅に向上させてます。

これまでは「TensorFlowで歴代「クラウン」の画像を判別させてみた」の記事でも参照させていただいた「TensorFlowでアニメゆるゆりの制作会社を識別する - kivantium活動日記」のコードをほぼそのまま流用させていただいてます。

これを使って”画像認識”の学習データを作るためには

(1) プログラムコードとTensorFlow実行環境を準備

(2) 教師データとなる画像と、その画像がなんの画像かをラベル分け

(3) (2)で集めた画像を訓練用(train)とテスト用(test)に分類(大体9対1)

(4) (3)の一覧リストを書いたファイル(train.txt、test.txt)を作成

という流れで、この最初の準備が大変でした。

おまけにデータを追加しようとすると、また(3)、(4)もやり直す羽目になり、これがなかなか面倒。

(1)、(2)は省きようがありませんけど、今回この面倒な(3)、(4)の部分を自動化することができました。

教師データ(今回は”けもフレ”キャラ顔画像)を準備し、それぞれがどんなキャラなのかを書いたフォルダに入れれば、勝手に(3)、(4)をやってくれるようにしました。

環境構築からの流れを以下に書きます。

(1) Windows上にTensorFlow実行環境を作成

TensorFlowがWindowsサポートしたのでインストールしてみた - デジタル・デザイン・ラボラトリーな日々

現状では、上のリンクのように「Anaconda for Windows」を入れて「TensorFlow」を入れるのが最も楽です。

今回のコードはOpenCVも使うため、上の操作に加えて

> conda install -c https://conda.binstar.org/menpo opencv3

という操作もお忘れなく。

今回はAnaconda 4.2.0 64ビット版 + TensorFlow 0.12.0が前提になってます。最新版では未確認です。

(2) ”教師データ”画像を集める

画像データを集めます。けもフレのフレンズさんの顔の部分を正方形に切り取ったものをひたすら集めてください。

動画やネットからひたすら手作業で集めてももちろんOKですが。

せっかくなんで、先回書いた「OpenCVによる”アニメ顔”検出」の記事にある通り、動画から自動的に画像を取り出させました。

取り出した画像を、フレンズさんの名前を書いたフォルダごとに分類。

Tfkemofr01

フォルダ名ですが、Windows版Anaconda+TensorFlowでは日本語が認識できないため、上のように英語表記でお願いします。

訓練させるためには、1キャラ当たり最低30枚は準備してください。正面向きの、目を閉じていないやつをチョイス。

Tfkemofr02

そのフォルダを「data」フォルダに入れて、学習用コード(cnn_train_56_a.py)と同じ階層に配置します。

注意点が2つ。

1、教師データのフォルダには画像データ以外入れない

2、各フォルダには最低でも10枚(できれば30枚以上)の画像データを入れる

これをやらないと、次の(3)のプログラムが動きません。ご注意を。

(3) 訓練実行

> python cnn_train_56_a.py

と実行すれば、各教師データからプログラムが勝手に訓練用・テスト用(9:1に分割)に分けてくれます(プログラム本体は下に載せてます)。

まさにプログラム実行者はただ教師データを準備すればいいんです!

ディープラーニングというよりスピード〇ーニングっぽいフレーズですね。

(4) 判別実行

上の訓練データを用いて、早速判別をさせてみます。

Tfkemofr03

訓練用コード「cnn_train_56_a.py」と同じ階層に、上の二つのファイル・フォルダを入れておきます。

cnn_app_56_a.py」はコード本体。「analysis」フォルダには上の教師データとは違う画像を入れておきます(コードは下に載せてます)。

Tfkemofr04

ネット上から適当に集めました。

実行結果はのちほど。

ここで訓練用コード「cnn_train_56_a.py」と判別用コード「cnn_app_56_a.py」を載せておきます。

◆ cnn_train_56_a.py

#!/usr/bin/env python

import glob
import os
import sys
import cv2
import numpy as np
import tensorflow as tf
import tensorflow.python.platform

path=os.getcwd()+'/data/'
class_count = 0
folder_list=os.listdir(path)

for folder in folder_list:

  class_count = class_count+1

NUM_CLASSES = class_count
IMAGE_SIZE = 56
IMAGE_PIXELS = IMAGE_SIZE*IMAGE_SIZE*3

flags = tf.app.flags
FLAGS = flags.FLAGS

flags.DEFINE_string('label', 'label.txt', 'File name of label')
flags.DEFINE_string('train_dir', './', 'Directory to put the training data.')
flags.DEFINE_integer('max_steps', 100, 'Number of steps to run trainer.')
flags.DEFINE_integer('batch_size', 20, 'Batch size'
                     'Must divide evenly into the dataset sizes.')
flags.DEFINE_float('learning_rate', 1e-4, 'Initial learning rate.')

def inference(images_placeholder, keep_prob):

    def weight_variable(shape):
      initial = tf.truncated_normal(shape, stddev=0.1)
      return tf.Variable(initial)

    def bias_variable(shape):
      initial = tf.constant(0.1, shape=shape)
      return tf.Variable(initial)

    def conv2d(x, W):
      return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

    def max_pool_2x2(x):
      return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                            strides=[1, 2, 2, 1], padding='SAME')

    x_image = tf.reshape(images_placeholder, [-1, 56, 56, 3])

    with tf.name_scope('conv1') as scope:
        W_conv1 = weight_variable([3, 3, 3, 32])
        b_conv1 = bias_variable([32])
        h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

    with tf.name_scope('pool1') as scope:
        h_pool1 = max_pool_2x2(h_conv1)

    with tf.name_scope('conv2') as scope:
        W_conv2 = weight_variable([3, 3, 32, 64])
        b_conv2 = bias_variable([64])
        h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)

    with tf.name_scope('pool2') as scope:
        h_pool2 = max_pool_2x2(h_conv2)

    with tf.name_scope('conv3') as scope:
        W_conv3 = weight_variable([3, 3, 64, 128])
        b_conv3 = bias_variable([128])
        h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3)

    with tf.name_scope('pool3') as scope:
        h_pool3 = max_pool_2x2(h_conv3)

    with tf.name_scope('fc1') as scope:
        W_fc1 = weight_variable([7*7*128, 1024])
        b_fc1 = bias_variable([1024])
        h_pool3_flat = tf.reshape(h_pool3, [-1, 7*7*128])
        h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1)

        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    with tf.name_scope('fc2') as scope:
        W_fc2 = weight_variable([1024, NUM_CLASSES])
        b_fc2 = bias_variable([NUM_CLASSES])

    with tf.name_scope('softmax') as scope:
        y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

    return y_conv

def loss(logits, labels):

    cross_entropy = -tf.reduce_sum(labels*tf.log(logits))

    tf.summary.scalar("cross_entropy", cross_entropy)
    return cross_entropy

def training(loss, learning_rate):

    train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss)
    return train_step

def accuracy(logits, labels):

    correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
    tf.summary.scalar("accuracy", accuracy)
    return accuracy

if __name__ == '__main__':

    count=0
    folder_list=os.listdir(path)

    train_image = []
    train_label = []
    test_image = []
    test_label = []

    f = open(FLAGS.label, 'w')
    for folder in folder_list:
      subfolder = os.path.join(path,folder)
      file_list = os.listdir(subfolder)

      filemax = 0

      for file in file_list:
        filemax = filemax + 1

# train : test = 9 : 1
      file_rate = int(filemax/10*9)

      i = 0

      for file in file_list:

        img = cv2.imread('./data/' + folder + '/' + file)
        img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE))
        if i <= file_rate:
           train_image.append(img.flatten().astype(np.float32)/255.0)
           tmp = np.zeros(NUM_CLASSES)
           tmp[int(count)] = 1
           train_label.append(tmp)
        else:
           test_image.append(img.flatten().astype(np.float32)/255.0)
           tmp = np.zeros(NUM_CLASSES)
           tmp[int(count)] = 1
           test_label.append(tmp)

        i = i + 1

      label_name = folder + '\n'
      f.write(label_name)
      count=count+1
    f.close()

    train_image = np.asarray(train_image)
    train_label = np.asarray(train_label)
    test_image = np.asarray(test_image)
    test_label = np.asarray(test_label)

    with tf.Graph().as_default():

        images_placeholder = tf.placeholder("float", shape=(None, IMAGE_PIXELS))
        labels_placeholder = tf.placeholder("float", shape=(None, NUM_CLASSES))

        keep_prob = tf.placeholder("float")

        logits = inference(images_placeholder, keep_prob)

        loss_value = loss(logits, labels_placeholder)

        train_op = training(loss_value, FLAGS.learning_rate)

        acc = accuracy(logits, labels_placeholder)

        saver = tf.train.Saver()

        sess = tf.Session()

        sess.run(tf.global_variables_initializer())

        summary_op = tf.summary.merge_all()
        summary_writer = tf.summary.FileWriter(FLAGS.train_dir, sess.graph)

        for step in range(FLAGS.max_steps):
            for i in range(int(len(train_image)/FLAGS.batch_size)):

                batch = FLAGS.batch_size*i

                sess.run(train_op, feed_dict={
                  images_placeholder: train_image[batch:batch+FLAGS.batch_size],
                  labels_placeholder: train_label[batch:batch+FLAGS.batch_size],
                  keep_prob: 0.5})

            train_accuracy = sess.run(acc, feed_dict={
                images_placeholder: train_image,
                labels_placeholder: train_label,
                keep_prob: 1.0})
            print ("step %d, training accuracy %g"%(step, train_accuracy))

            summary_str = sess.run(summary_op, feed_dict={
                images_placeholder: train_image,
                labels_placeholder: train_label,
                keep_prob: 1.0})
            summary_writer.add_summary(summary_str, step)

    print ("test accuracy %g"%sess.run(acc, feed_dict={
        images_placeholder: test_image,
        labels_placeholder: test_label,
        keep_prob: 1.0}))

    save_path = saver.save(sess, "./model.ckpt")

◆ cnn_app_56_a.py

#!/usr/bin/env python

import glob
import os
import sys
import numpy as np
import tensorflow as tf
import cv2

path=os.getcwd()+'/analysis/'
file_list=os.listdir(path)

i = 0
label_name = []

flags = tf.app.flags
FLAGS = flags.FLAGS
flags.DEFINE_string('label','label.txt','File name of label')

f = open(FLAGS.label,'r')
for line in f:
  line = line.rstrip()
  l = line.rstrip()
  label_name.append(l)
  i = i + 1

NUM_CLASSES = i
IMAGE_SIZE = 56
IMAGE_PIXELS = IMAGE_SIZE*IMAGE_SIZE*3

def inference(images_placeholder, keep_prob):

    def weight_variable(shape):
      initial = tf.truncated_normal(shape, stddev=0.1)
      return tf.Variable(initial)

    def bias_variable(shape):
      initial = tf.constant(0.1, shape=shape)
      return tf.Variable(initial)

    def conv2d(x, W):
      return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

    def max_pool_2x2(x):
      return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                            strides=[1, 2, 2, 1], padding='SAME')

    x_image = tf.reshape(images_placeholder, [-1, 56, 56, 3])

    with tf.name_scope('conv1') as scope:
        W_conv1 = weight_variable([3, 3, 3, 32])
        b_conv1 = bias_variable([32])
        h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)

    with tf.name_scope('pool1') as scope:
        h_pool1 = max_pool_2x2(h_conv1)

    with tf.name_scope('conv2') as scope:
        W_conv2 = weight_variable([3, 3, 32, 64])
        b_conv2 = bias_variable([64])
        h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)

    with tf.name_scope('pool2') as scope:
        h_pool2 = max_pool_2x2(h_conv2)

    with tf.name_scope('conv3') as scope:
        W_conv3 = weight_variable([3, 3, 64, 128])
        b_conv3 = bias_variable([128])
        h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3)

    with tf.name_scope('pool3') as scope:
        h_pool3 = max_pool_2x2(h_conv3)

    with tf.name_scope('fc1') as scope:
        W_fc1 = weight_variable([7*7*128, 1024])
        b_fc1 = bias_variable([1024])
        h_pool3_flat = tf.reshape(h_pool3, [-1, 7*7*128])
        h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1)
        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    with tf.name_scope('fc2') as scope:
        W_fc2 = weight_variable([1024, NUM_CLASSES])
        b_fc2 = bias_variable([NUM_CLASSES])

    with tf.name_scope('softmax') as scope:
        y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

    return y_conv

if __name__ == '__main__':
    test_image = []
    test_filenm = []

    for file in file_list:
        test_filenm.append(file)

        img = cv2.imread('./analysis/' + file )
        img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE))
        test_image.append(img.flatten().astype(np.float32)/255.0)

    test_image = np.asarray(test_image)

    images_placeholder = tf.placeholder("float", shape=(None, IMAGE_PIXELS))
    labels_placeholder = tf.placeholder("float", shape=(None, NUM_CLASSES))
    keep_prob = tf.placeholder("float")

    logits = inference(images_placeholder, keep_prob)
    sess = tf.InteractiveSession()

    saver = tf.train.Saver()
    sess.run(tf.global_variables_initializer())
    saver.restore(sess, "./model.ckpt")

    for i in range(len(test_image)):
        accr = logits.eval(feed_dict={
            images_placeholder: [test_image[i]],
            keep_prob: 1.0 })[0]
        pred = np.argmax(logits.eval(feed_dict={
            images_placeholder: [test_image[i]],
            keep_prob: 1.0 })[0])

        pred_label = label_name[pred]
#        print (pred,accr)
        print (test_filenm[i],' , ',pred_label)

あまりきれいなコードではありませんが・・・これをそれぞれコピペしてお使いください。

ちなみに、ベースは「TensorFlowの画像認識コードを高解像度化してみた: EeePCの軌跡」で使った56×56のを使ってます。

さすがに今回の教師用データを配布するわけにはいかないので、こちらはご自分でご用意願います。

で、今回用意したフレンズさんの種類は27種類

8話までの画像を使ったんですが、すでにこの数です。

30枚以上の顔画像が取れたフレンズさんしか使わなかったんですけど、いったい全部で何種類出てるんでしょうかね?

なお、「OpenCVによる”アニメ顔”検出」を使った顔画像検出でほとんどのキャラは抽出できたんですが、ハシビロコウさんライオンさんマーゲイさんはほとんど抽出できず。結局、手で集めました。

ハシビロコウさんは特に顔の表情がなさすぎるので、画像集めには結構苦労します。

サーバルちゃんとカバンちゃんはさすがに全話登場しているため何百枚でも集まってしまうのですが、他とのバランスで100枚程度まで絞ってます。

27種類で200サイクルの学習を実行、これをメインPC(Corei3-3220 3.3GHz)で学習させたら2時間かかりました。うわーったいへんだねー。

で、判別した結果は

Kemono04

左が画像のファイル名、右が判定結果。

黄色が正解だった項目を示してます。

31枚中、正解は21枚。判別率67.7%。

同人系の画像の判別率が半分程度と低かったですね。もうちょっと教師データがあれば認識率上がるかもしれませんが。ハカセと助手も認識率低く、ほとんど外れでした。

なぜか”マーゲイ”さんに判定される事例が多いのが気になるところ。

あの顔って、もしかしてけものフレンズでは”標準形”なんでしょうか?

で、ちょっと調子に乗ってこんな実験も。

Kemono06

デジカメで撮った画像から取り出したリアルな人の顔画像で判別させてみました。

幼稚園の運動会の画像を使ったせいか、様々な顔を大量に取得(約100枚)。

Kemono05

結果はこんな感じ(名前の後ろの数字は気にしないでください)。

カバンちゃん、コウテイペンギンさん、イワトビペンギンさんが多いですね。スナネコ、ライオンもちょくちょく。

なお、私の顔写真が3枚混じってましたが、判定は「カバン」×2、「コウテイペンギン」×1。私はどうやら”カバンちゃん”らしいです。

なぜか次男は”トキさん”に分類されることが多いですね。確かに音痴ですが。

妻と長男は「コウテイペンギンさん」と無難な(?)分類結果に。

先のOpenCVでの顔検出のコード等と組み合わせて、顔を撮影すると「君は〇〇なフレンズだね!」って言ってくれるやつを作ろうかとも考えてますが。

正直、大して似ていない画像に判別されることが多いので、ちょっとモチベーション下がってます。

なお、まだ「けものフレンズ」は終わっていないので、今後画像を増やすことも可能。まだ8話までのデータですし。

今回、いちいち学習データの一覧リストを作らなくても「data」フォルダに画像ファイルを入れたフォルダを放り込んだら勝手に訓練できるようになったので、判別器のアップデートがずいぶん楽になりました。

同じコードを私の職場にも送って使ってますが、画像データが増えたらすぐ実行すればいいだけなので、運用も楽になりました。もっとも、私しか使ってませんけど。

将来的には、画像が増えたら所定の場所に登録してもらい、毎晩訓練コードを自動起動させて、翌日にはアップデートされた画像認識が使える、なんて運用も可能にできます。

ちょっと手軽にWindows上でTensorFlowの画像認識を実験してみたいという方、ぜひこちらのコード、おすすめです。

けものフレンズBD付オフィシャルガイドブック (1)

« 百均のiPhoneケースもアイデア次第で豪華に!? | トップページ | Raspberry Pi Zero向け小型カメラモジュール »

Raspberry Pi・Arduino・電子工作」カテゴリの記事

コメント

コメントを書く

(ウェブ上には掲載しません)

トラックバック


この記事へのトラックバック一覧です: TensorFlowで「けものフレンズ」の”フレンズ判別器”作ってみた:

« 百均のiPhoneケースもアイデア次第で豪華に!? | トップページ | Raspberry Pi Zero向け小型カメラモジュール »

無料ブログはココログ

スポンサード リンク

ブログ村