かなで技術日誌

プログラミングやエンジニアリング周りについて

主なアウトプットはScrapboxObsidianにまとめてます。

機械学習でゆるキャンΔのなでりんを判別

GitHubリポジトリはこちらです。
github.com


なんとか「ゼロから作るDeep Learning」を読み終わり、なんとなく理解したところで、まずは実装してみようと思いました。

画像認識や自然言語処理音声認識などなどありますが、CNNについて勉強したのと割と定番でもあるので画像認識の実装をしようと思いました。
ライブラリ(フレームワーク?)でよく使われる定番としては
・TensorFlow
・Chainer
・PyTorch
があると思います。
今回は、一番使用率が高いであろうTensorFlowを使いました。

環境
・Windows10
・Python3.6
・TensowFlow1.8.0
・PyCharm2018.1.2

ゆるキャンΔはJKがただキャンプをするだけのアニメです
しまりんが好きです
今回はなでしこ/しまりん/その他キャラクターで分類しましたが、二値分類にしないなら全員分類してもよかったのではと思っています
なおゆるキャンΔを選んだ理由は(たぶん)誰もまだブログでは実装して公開していなかったので・・・


参考記事(パクリ記事)
画像取得と顔の切り出しは
blog.aidemy.net
画像の水増し方法も参考になった記事があったが見つからない・・・(見つけたら追記します)
実装部分は
機械学習でNEW GAME!のキャラを判別してみた | こんにゃくの日記


手順としては
①画像を収集
②顔部分を切り出す
③データを増やす
④学習させる
⑤未知の画像を判定させる
となっています。

最終的なディレクトリ構成はGitHubリポジトリを見てください
(ブログだと綺麗に表示できなかった)

①画像を収集

これはGoogle Custom Search APIでさくっと集められました。

import urllib.request
from urllib.parse import quote
import httplib2
import json
import os
import cv2
import sys
import shutil

# keywordsの画像のurlを取得後、jpg画像に変換しファイルにどんどん入れてく
# 全5人100個ずつ取得
API_KEY = ""
CUSTOM_SEARCH_ENGINE = ""

keywords = ["各務原なでしこ", "志摩リン", "斉藤恵那", "大垣千明", "犬山あおい"]


def get_image_url(search_item, total_num):
    img_list = []
    i = 0
    while i < total_num:
        query_img = "https://www.googleapis.com/customsearch/v1?key=" + API_KEY + "&cx=" + CUSTOM_SEARCH_ENGINE + "&num=" + str(
            10 if (total_num - i) > 10 else (total_num - i)) + "&start=" + str(i + 1) + "&q=" + quote(
            search_item) + "&searchType=image"
        res = urllib.request.urlopen(query_img)
        data = json.loads(res.read().decode('utf-8'))
        for j in range(len(data["items"])):
            img_list.append(data["items"][j]["link"])
        i += 10
    return img_list


def get_image(search_item, img_list, j):
    opener = urllib.request.build_opener()
    http = httplib2.Http(".cache")
    for i in range(len(img_list)):
        try:
            fn, ext = os.path.splitext(img_list[i])
            print(img_list[i])
            response, content = http.request(img_list[i])
            filename = os.path.join("./origin_image", str("{0:02d}".format(j)) + "." + str(i) + ".jpg")
            with open(filename, 'wb') as f:
                f.write(content)
        except:
            print("failed to download the image.")
            continue


for j in range(len(keywords)):
    print(keywords[j])
    img_list = get_image_url(keywords[j], 100)
    get_image(keywords[j], img_list, j)

API_KEYとCUSTOM_SEARCH_ENGINEには各々取得したものを設定してください。
ただ無料の範囲内では一日あたり100クエリまでのようで、これだけで機械学習をするには課金しないといけなさそうです
今回は一度使って慣れることが目的なので無料で使いました。

ちなみに集めるとこんな感じになります。
f:id:kana_kanade:20180503230922p:plain


可愛いし機械学習とかもうどうでもいいのでは?

と思ってしまいましたが、正気に戻って作業を続けます。

②顔部分を切り出す
これはOpenCVとAnimeFaceを使いました。
アニメ顔検出にlbpcascade_animeface.xmlはを使いました。ここからダウンロードできます
GitHub - nagadomi/lbpcascade_animeface: A Face detector for anime/manga using OpenCV

import numpy as np
import cv2
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import glob
import os

# 元画像を取り出して顔部分を正方形で囲み、64×64pにリサイズ、別のファイルにどんどん入れてく
in_dir = "./image/*"
out_dir = "./face_image"
in_jpg = glob.glob(in_dir)
in_fileName = os.listdir("./image/")
# print(in_jpg)
# print(in_fileName)
print(len(in_jpg))
for num in range(len(in_jpg)):
    image = cv2.imread(str(in_jpg[num]))
    if image is None:
        # print("Not open:", line)
        print("Not open")
        continue

    image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    cascade = cv2.CascadeClassifier("lbpcascade_animeface.xml")
    # 顔認識の実行
    face_list = cascade.detectMultiScale(image_gs, scaleFactor=1.1, minNeighbors=2, minSize=(64, 64))
    # 顔が1つ以上検出された時
    if len(face_list) > 0:
        for rect in face_list:
            x, y, width, height = rect
            image = image[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]]
            if image.shape[0] < 64:
                continue
            image = cv2.resize(image, (64, 64))
    # 顔が検出されなかった時
    else:
        print("no face")
        continue
    print(image.shape)
    # 保存
    # fileName=os.path.join(out_dir,str(in_fileName[num])+".jpg")
    fileName = os.path.join(out_dir, str(in_fileName[num]))
    cv2.imwrite(str(fileName), image)

in_dir = "./face_image/*"
in_jpg = glob.glob(in_dir)
img_file_name_list = os.listdir("./face_image/")
# img_file_name_listをシャッフル、そのうち2割をtest_imageディテクトリに入れる
np.random.shuffle(in_jpg)
import shutil

for i in range(len(in_jpg) // 5):
    shutil.move(str(in_jpg[i]), "./test_image")

これで顔部分が繰り出された画像が生成されます。
ここで顔じゃないものが検出されていたら、予め削除しておきます。


③データを増やす

そしてこれだけではデータ数が少なすぎるため反転させたり回転させて増やします

import os
from PIL import Image


def readImg(imgName):
    try:
        img_src = Image.open("face_image/" + imgName)
        print("read img!")
    except:
        print("{} is not image file!".format(imgName))
        img_src = 1
    return img_src


def spinImg(imgNames):
    for imgName in imgNames:
        img_src = readImg(imgName)
        if img_src == 1:continue
        else:
            #上下反転
            tmp = img_src.transpose(Image.FLIP_TOP_BOTTOM)
            tmp.save("flipTB_" + imgName)
            #90度回転
            tmp = img_src.transpose(Image.ROTATE_90)
            tmp.save("spin90_" + imgName)
            #270度回転
            tmp = img_src.transpose(Image.ROTATE_270)
            tmp.save("spin270_" + imgName)
            #左右反転
            tmp = img_src.transpose(Image.FLIP_LEFT_RIGHT)
            tmp.save("flipLR_" + imgName)
            print("{} is done!".format(imgName))


#read imgs names
imgNames = os.listdir("face_image")#画像が保存されてるディレクトリへのpathを書きます
print(imgNames)
spinImg(imgNames)

かなり増やしましたが、しまりんで300枚いかないぐらいでなでしこは200枚いかないので数は少ないのですが、とりあえずこれで実装してみます

④学習させる
では実際に学習させていきます。

#!/usr/bin/env python
import os
import cv2
import numpy as np
import tensorflow as tf


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', './tmp/data', 'Directory to put the training data.')
flags.DEFINE_integer('max_steps', 120, 'Number of steps to run trainer.')
flags.DEFINE_integer('batch_size', 20, 'Batch size'
                     'Must divide evenly into the dataset sizes.')
# accuracyが変化しなかったため1e-4から変更しました
flags.DEFINE_float('learning_rate', 1e-5, 'Initial learning rate.')


# 予測モデルを作成する関数
def inference(images_placeholder, keep_prob):

    # 重みを標準偏差0.1の正規分布で初期化する
    def weight_variable(shape):
        initial = tf.truncated_normal(shape, stddev=0.1)
        return tf.Variable(initial)

    # バイアスを標準偏差0.1の正規分布で初期化
    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')

    # 入力を56x56x3に変形
    x_image = tf.reshape(images_placeholder, [-1, 56, 56, 3])
    # 畳み込み層1の作成
    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)
    # プーリング層1の作成
    with tf.name_scope('pool1') as scope:
        h_pool1 = max_pool_2x2(h_conv1)
    # 畳み込み層2の作成
    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)
    # プーリング層2の作成
    with tf.name_scope('pool2') as scope:
        h_pool2 = max_pool_2x2(h_conv2)
    # 畳み込み層3の作成
    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)
    # プーリング層3の作成
    with tf.name_scope('pool3') as scope:
        h_pool3 = max_pool_2x2(h_conv3)
    # 全結合層1の作成
    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)
    # 全結合層2の作成
    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


# lossを計算する関数
def loss(logits, labels):
    cross_entropy = -tf.reduce_sum(labels*tf.log(logits))
    tf.summary.scalar("cross_entropy", cross_entropy)
    return cross_entropy


# 訓練のOpを定義する関数
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():
        # 画像を入れる仮のTensor
        images_placeholder = tf.placeholder("float", shape=(None, IMAGE_PIXELS))
        # ラベルを入れる仮のTensor
        labels_placeholder = tf.placeholder("float", shape=(None, NUM_CLASSES))
        # dropout率を入れる仮のTensor
        keep_prob = tf.placeholder("float")

        # inference()を呼び出してモデルを作成
        logits = inference(images_placeholder, keep_prob)
        # loss()を呼び出して損失を計算
        loss_value = loss(logits, labels_placeholder)
        # training()を呼び出して訓練
        train_op = training(loss_value, FLAGS.learning_rate)
        # 精度の計算
        acc = accuracy(logits, labels_placeholder)
        # 保存の準備
        saver = tf.train.Saver()
        # Sessionの作成
        sess = tf.Session()
        # 変数の初期化
        sess.run(tf.global_variables_initializer())
        # TensorBoardで表示する値の設定
        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_size文の画像に対して訓練の実行
                batch = FLAGS.batch_size*i
                # Feed_deicでplaceholderに入れるデータを指定する
                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})
            # 1step終わるたびに精度を計算する
            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))
            # 1step終わるたびにTensorBoardに表示する値を追加する
            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")

本当は、あらかじめコマンドプロンプトorターミナルで

tensorboard --logdir /tmp/data

と入力するとTensorBoardで確認できるようなのですが、まだうまくいっていません。。。

accuracyが低いまま(0.15とか)学習が進まないことがありましたが、learning_rateを1e-5に下げることで解決できました

これで学習させると
step 0, training accuracy 0.576119
step 1, training accuracy 0.571144
step 2, training accuracy 0.571144
step 3, training accuracy 0.570149
...
...
step 117, training accuracy 0.972139
step 118, training accuracy 0.972139
step 119, training accuracy 0.973134
test accuracy 0.845455

となり、正解率が訓練データで97%、テストデータで84%となりました

自分のしょぼいノートPCでは風呂に入って出てもまだ学習が終わっていませんでした。。。

そして過学習気味っぽいように見えますがどうなんでしょうか

⑤未知の画像を判定させる
analysisフォルダに判定させたい画像を入れておいて、judge.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 (test_filenm[i],' , ',pred_label)

結果としてしまりんは一応全部正解していました

ただなでしこをよく間違えます

f:id:kana_kanade:20180503234250j:plain
これはなでしこ

f:id:kana_kanade:20180503234301j:plain
これはother

f:id:kana_kanade:20180503234325j:plain
これもother


しまりんはほぼ正しく判定できているが、なでしこはうまく判定できていない理由としては恐らくデータセットが少ないことから過学習しているのではないかと思います

訓練用データとテスト用データで分ける前にデータを水増しすることが果たして適切なのかも微妙ですが、他の事例を見てもしまりんの倍以上はデータセットが必要そうなので、データの数を増やすことで解決できないかもうちょっと試行錯誤したいです

あとは少ないデータセットの場合はアンサンブルも効果的かもしれない

雑な記事になってしまいました。
また改良して精度を上げたいと思います。