読者です 読者をやめる 読者になる 読者になる

bohemia日記

おうちハックとか画像処理、DeepLearningなど

ディープラーニングでおそ松さんの六つ子は見分けられるのか 〜実施編〜

ディープラーニング アニメ

前回、おそ松さんたちをディープラーニングで見分けるため、準備編としておそ松さんたちの顔画像を5644枚集めました。 今回はそれを用いて、ディープラーニングで学習させ、判別器を作って検証します。

集めた画像

人物 枚数
おそ松 1126 f:id:bohemian916:20151121163835p:plain:w100
から松 769 f:id:bohemian916:20151121163952p:plain:w100
チョロ松 1047 f:id:bohemian916:20151121164053p:plain:w100
一松 736 f:id:bohemian916:20151121164114p:plain:w100
十四松 855 f:id:bohemian916:20151121164151p:plain:w100
とど松 729 f:id:bohemian916:20151121164220p:plain:w100
その他 383

使用フレームワーク

最近GoogleからTensorFlowという新しいディープラーニングのフレームワークが発表されました。 会社のブログに使い方書いたのですが、まだ慣れていないので、今回はchainerを使います。こちらだとすぐに高い成果を上げているImageNetのNINモデル、4層畳み込みニューラルネットワークがサンプルで入っていますので、こちらを改良して使います。

imageNetの使い方は、こちらこちらを参考にしています。

訓練データセット

ImageNetで学習を行うには、train.txtとtest.txtの2つが必要です。 どちらも

/path/to/image/hoge1.jpg 1
/path/to/image/hoge2.jpg 5
/path/to/image/hoge3.jpg 2
/path/to/image/hoge4.jpg 4

という、画像パス名 ラベル番号 といった形式をしています。

集めた画像のうち、85%を学習データ、15%をテスト用データにします。

学習に用いるソース

基本的には、ImageNetのソースを使いますが、いくつか変更を施します。 まずGPUで学習させたあとに生成されるモデルをCPUでも使えるようにして保存します。 これはtrain_imagenet.pyの最終モデルを保存するときに、cpuで使える形式にしてから保存します。

# 最終行を変更
# Save final model
model.to_cpu()
pickle.dump(model, open(args.out, 'wb'), -1)

また、nin.pyには直接解析結果を返してくれるメソッドがないので、スコアをソフトマックス関数で処理した結果を返すメソッドを追加します。こちらを参考にしました

def predict(self, x_data, train=False):
        x = chainer.Variable(x_data, volatile=True)

        h = F.relu(self.conv1(x))
        h = F.relu(self.conv1a(h))
        h = F.relu(self.conv1b(h))
        h = F.max_pooling_2d(h, 3, stride=2)
        h = F.relu(self.conv2(h))
        h = F.relu(self.conv2a(h))
        h = F.relu(self.conv2b(h))
        h = F.max_pooling_2d(h, 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv3a(h))
        h = F.relu(self.conv3b(h))
        h = F.max_pooling_2d(h, 3, stride=2)
        h = F.dropout(h, train=train)
        h = F.relu(self.conv4(h))
        h = F.relu(self.conv4a(h))
        h = F.relu(self.conv4b(h))
        h = F.reshape(F.average_pooling_2d(h, 6), (x_data.shape[0], 1000))
        return F.softmax(h)

学習

それでは平均画像を生成してから、学習を開始します。 画像は、サイズを256*256にする必要があるため、こちらのcrop.pyを使ってサイズ調整を行いました。

$ ./crop.py /path/to/image/directory /path/to/image/directory
$ ./compute_mean.py train.txt --root /path/to/image/directory
$ ./train_imagenet.py train.txt test.txt --batchsize 14 --val_batchsize 80 --epoch 50 --gpu 0 --root /path/to/image/directory

訓練データでの結果

ログデータから訓練データにおけるエラー率の推移はこのようになりました。 プロットはこちらのplot.pyを使わせていただきました。 f:id:bohemian916:20151122172627p:plain

10000イテレーションあたりでほとんど下がりきっています。
trainでの最終的なエラーレートは0.02857
valでは0.0360です。96.4%の精度がでています。

おそ松さん判別器について

上記で作成した顔の分類器(ディープラーニング部)と、前記事で作った顔検出器(HOG+SVM)を用いて、キャプチャ画像から認識できるようにします。 処理手順としては、
1, 画像サイズを調整する
2, 顔検出器で顔を切り抜く
3, 切り抜いた顔を分類器にかける
4, 元画像に結果を表示する

というようになります。 以下がそのソースです。

#!/usr/bin/env python
#! -*- coding: utf-8 -*-

import os
import sys
import dlib
from skimage import io
import numpy as np
import cv2
import argparse
import os
from PIL import Image
import six
import cPickle as pickle
from six.moves import queue

parser = argparse.ArgumentParser(
    description='Learning convnet from ILSVRC2012 dataset')
parser.add_argument('image', help='Path to image')
parser.add_argument('--mean', '-m', default='mean.npy',
                    help='Path to the mean file (computed by compute_mean.py)')
parser.add_argument('--detector', '-d', default='detector.svm',
                    help='Path to the detector file ')
parser.add_argument('--model', '-mo', default='model',
                    help='Path to the model file')
args = parser.parse_args()

# ファイル読み込み
detector = dlib.simple_object_detector(args.detector)

img = cv2.imread(args.image,1)
import nin
model = pickle.load(open(args.model,'rb'))
mean_image = pickle.load(open(args.mean, 'rb'))
categories = np.loadtxt("labels.txt", str, delimiter="\t")
cropwidth = 256 - model.insize 

out = img.copy()

# 画像サイズ変更
def resize(img):
    target_shape = (256, 256)
    height, width, depth = img.shape
    print "height:"+str(height) + "width:" + str(width)
    output_side_length=256
    new_height = output_side_length
    new_width = output_side_length
    if height > width:
        new_height = output_side_length * height / width
    else:
        new_width = output_side_length * width / height

    resized_img = cv2.resize(img, (new_width, new_height))
    height_offset = (new_height - output_side_length) / 2
    width_offset = (new_width - output_side_length) / 2
    cropped_img = resized_img[height_offset:height_offset + output_side_length,
    width_offset:width_offset + output_side_length]

    return cropped_img

def read_image(src_img, center=True, flip=False):
    # Data loading routine
    image = np.asarray(Image.open(src_img)).transpose(2, 0, 1)
    #image = src_img.transpose(2, 0, 1)
    if center:
        top = left = cropwidth / 2
    else:
        top = random.randint(0, cropwidth - 1)
        left = random.randint(0, cropwidth - 1)
    bottom = model.insize + top
    right = model.insize + left

    image = image[:, top:bottom, left:right].astype(np.float32)
    image -= mean_image[:, top:bottom, left:right]
    image /= 255
    return image

# 顔検出
dets = detector(img)
print "faces:" + str(len(dets))
height, width, depth = img.shape
x = np.ndarray((len(dets), 3, model.insize, model.insize), dtype=np.float32)
faces = [() for i in range(len(dets))]

for i,d in enumerate(dets):
    # 顔領域の調整
    f_top    = max((0, d.top()))
    f_bottom = min((d.bottom(), height -1))
    f_left   = max((0, d.left()))
    f_right  = min((d.right(), width -1))
 
    print "%d %d %d %d" %(f_top, f_bottom, f_left, f_right)
    faces[i] = (f_top, f_bottom, f_left, f_right)

    face_img = img[f_top:f_bottom, f_left:f_right]
    resized_face = resize(face_img)
    cv2.imwrite("temp.jpg", resized_face)
    x[i] = read_image("temp.jpg")

# 顔の分類スコア取得
scores = model.predict(x)

# 結果表示
face_info = []
for i,face in enumerate(faces):

    prediction = zip(scores.data[i], categories)
    prediction.sort(cmp=lambda x, y: cmp(x[0], y[0]), reverse=True)
    score, name = prediction[0]
    for j in range(6):
        print ('%s score:%4.1f%%' % (prediction[j][1], prediction[j][0] * 100)) 

    if name == "osomatsu":
        color = (0,0,255)
    elif name == "karamatsu":
        color = (255,0,0)
    elif name == "choromatsu":
        color = (0,255,0)
    elif name == "ichimatsu":
        color = (133,22, 200)
    elif name == "jushimatsu":
        color = (0, 255,255)
    elif name == "todomatsu":
        color = (167, 160, 255)
    else :
        color = (255,255,255)

    cv2.rectangle(out, (face[2], face[0]), (face[3], face[1]), color, 3)
    cv2.putText(out,"%s" %(name),(face[2],face[1]+15),cv2.FONT_HERSHEY_COMPLEX, 0.5 ,color)
    cv2.putText(out,"%4.1f%%" %( score*100),(face[2],face[1]+30),cv2.FONT_HERSHEY_COMPLEX, 0.5 ,color)
cv2.imshow('image',out)
cv2.waitKey(0)
cv2.destroyAllWindows()

試してみる

訓練データ話数内より

まずは訓練データに含まれているのもから試してみます。認識結果は、それぞれのカラーで顔を囲うようにします。
もう一度確認しておくと、おそ松から松チョロ松一松十四松とど松、です。

OPから f:id:bohemian916:20151122142816p:plain 訓練データでのエラー率は数%なので、ちゃんと認識できています。

もう何枚か f:id:bohemian916:20151122151617p:plain

f:id:bohemian916:20151122143902p:plain

訓練データ話数外より

問題は、訓練データに含まれていない話数の顔が認識できるかどうかです。 5話の途中までを訓練データで用いたので、6話から抽出して解析してみます。

おじいちゃんの執事に一松と間違えられる十四松。ちゃんと十四松として認識されています! 画像見るだけじゃ自分もわからないのにすごい! f:id:bohemian916:20151122150619p:plain

かと思えば、一松をとど松として認識しています。 f:id:bohemian916:20151122145336p:plain

誤認識部分のスコアを見てみると、
todomatsu score:56.3%
karamatsu score:37.9%
ichimatsu score: 3.8%
osomatsu score: 1.9%
jushimatsu score: 0.2%
となっています。他の正確に認識できている顔のスコアはほぼ100%なので、それに比べれば確かさは低くなってはいます。 他の結果を見る限り、特徴の少ない画像は、とど松のスコアが高くなる傾向がある気がします。

こちらは全員ちゃんと認識されています。 f:id:bohemian916:20151122150146p:plain

一松以外みんなとど松になっています。 f:id:bohemian916:20151122150301p:plain

から松だらけ。自分も見分けやすい一松と十四松は正解率高め。 f:id:bohemian916:20151122163634p:plain

やってみて

正直、学習させてもあまり判別できないんじゃないかと懸念してたので、思ってたよりはちゃんと見分けられているという感想です。 人の目で見て分かりづらい顔画像は、おそらく

  • 画像における見分けられる顔の特徴が少ない
  • 見分けるポイントがわかっていない

のどちらかだと思います。前者は、そもそもちゃんと書き分けてない場合で、ディープラーニングで解析しても判断しづらいのでしょう。 後者は、ちゃんと描き分けてるけど、人がそれを理解できていない場合です。この場合は、ディープラーニングが特徴を見つけ出して判別できる、ということだと思います。

精度については、まだ改善の余地はあるかと思います。学習データである画像の質をあげる必要があります。キャプチャから切り取っていると、どうしても動かないキャラはずっと同じ画像になってしまうので、実質枚数があまり多くありません。例えば6人いる状態で、60フレームほどチョロ松のみが喋ったとします。すると60フレーム分切り取ると360枚分の顔画像が生成されるのですが、295枚は重複画像です。またチョロ松の60枚も、喋っているコマはせいぜい口閉じ、口半開け、口開けの3〜4種類程度しか存在しません。なので360枚中、実質9種類程度しかありません。 今回の画像収集で極力同一コマの画像は避けていたのですが、それでも多いです。 ですので、同じ画像は排除し、一つの画像からノイズをかけたり各種フィルタをかけて新たに画像を生成するのがよいかと思います。

また前記事で生成したおそ松さん用顔検出器の精度もあまりよくなく、取り逃がしている写真が結構あります。 ですので、検出器の精度を向上させるための顔画像選定も必要になってきます。 もう少し精度向上に向けて頑張ります。

参考

PFN発のディープラーニングフレームワークchainerで画像分類をするよ(chainerでニューラルネット1)

ChainerのNINで自分の画像セットを深層学習させて認識させる

Googleの公開した人工知能ライブラリTensorFlowを触ってみた