ちゅん顔の認識

dodosoft Advent Calendar、uphy担当2記事目!!

www.adventar.org

以前dodosoftの集まりで、船の上でBBQをやったことがありました。 とてもお洒落で素敵なBBQでした。まだまだ暑い時期の涼しい海、きれいな夜空、レインボーブリッジ、そしてたくさんのちゅん顔。どれも良い思い出…。

勉強会メンバーそれぞれの、思い思いのちゅん顔。もう何がちゅん顔なのか、そのころ分かりませんでした。

最近職場で通達されました。「明日からお前データサイエンティストな!」最近巷でよくあるあれです。データサイエンティストなんてそんな簡単になれるはずもなく、右も左も分からず、有識者もおらず、毎日四苦八苦しながらなんちゃって機械学習やってます。

せっかく仕事で機械学習やってるので、プライベートでもなにか学習してみるか!と思いたち、仕事ではブラックボックスであるために使いづらくてやってない、deep learningをやってみることにしました。

deep learningを使うと、何やら特徴量の抽出とかしなくても画像認識できるらしい。 という訳で、誰にも評価できなかったあの日のちゅん顔を、ここで白黒はっきりさせようと思いました。

環境構築

前回の記事でやりました。

uphy.hatenablog.com

まぁ、結局Dockerじゃなくてホスト側に環境作っちゃったんで使わないんですけど!

画像収集

まずは学習するための、ラベル付きのデータセットが必要です。つまり、画像と、その画像がちゅん顔なのか、非ちゅん顔なのかのデータセットです。

ちゅん顔と、ちゅん顔じゃない顔(非ちゅん顔)、スクレイピング活用しつつ、ひたすら探しまわりました。それはもう顔がゲシュタルト崩壊するほどに・・

uphy.hatenablog.com

探せば探すほど、ちゅん顔なのかそうじゃないのか分からなくなりました。 そこで、ちゅん顔とは何かをwebで調べつつ、ちゅん顔に関する基準を設けました。

  • 口が少し空いてる
  • 唇をすこし尖らせる
  • 目を大きく開いている
  • 真顔ではない
  • 笑顔ではない

非常に難しいです。もう挫折しそうです。 ほんとに頑張って、120枚ものちゅん顔を集めました。ちなみに普通の顔写真は、10倍くらい集まりました。

しかし、いろんな画像認識の記事見ましたが、120枚じゃ全然足りないそうです・・・

画像は、ちゅん顔を、faces/chunディレクトリに、非ちゅん顔を、faces/normalディレクトリに格納しました。

プログラム

画像の読み込み

faces/chunディレクトリにある、ちゅん顔画像集、faces/normalディレクトリにある、非ちゅん顔画像集を読み込む。

import cv2
from os import listdir
from os.path import join

def loadimage(file:str):
    image = cv2.imread(file)
    #image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    image = cv2.resize(image, (64, 64))
    image = image/255. 
    return (image, file)

def loadimages(dir:str):
    images = []
    files = []
    for f in listdir(dir):
        if not (f.endswith('.jpg') or f.endswith('.png')):
            continue
        file = join(dir, f)
        image, file = loadimage(file)
        images.append(image)
        files.append(file)
    return (images, files)

# load images
images_chun, files_chun = loadimages('faces/chun')
images_normal, files_normal = loadimages('faces/normal')

画像数調整

ちゅん顔と、非ちゅん顔の数を揃える。 数が離れていると、偏った学習をしてしまう。 当初、非ちゅん顔画像の方が多く、すべての画像を非ちゅん顔と学習プログラムが判断し、高認識率を求めようとしてしまってた。なので圧倒的に少ないちゅん顔画像の数に合わせます。

import numpy as np

while len(images_normal) > len(images_chun):
    i = np.random.randint(len(images_normal))
    del(images_normal[i])
    del(files_normal[i])

入力ベクトルを生成

images_chunとimages_normalを結合して、入力ベクトルxとする。

x = []
x.extend(images_chun)
x.extend(images_normal)
x = np.array(x)

ラベル生成

ちゅん顔は1、非ちゅん顔0というラベルを設定する。

y = []
y.extend([1]*len(images_chun))   # chun
y.extend([0]*len(images_normal)) # non-chun

出力ベクトル生成

ラベルから、出力ベクトルyを生成する。

上で、ちゅん顔は1、非ちゅん顔0というラベルを設定したが、それをKerasで扱いやすいようにするために、categorical形式(呼び方わからない)に変換する(np_utils.to_categorical())。こんな感じに変換してくれる。

ちゅん顔
y = [0, 1]
非ちゅん顔
y = [1, 0]

このようにしておくと、最終的な結果が、y = [ちゅん顔の確率, 非ちゅん顔の確率]という形で現れ分かりやすい。 今回は2クラスの分類なので、こうしなくても、回帰のように考えてもいいのかも?わからん。

from keras.utils import np_utils

y = np_utils.to_categorical(y, 2)

学習データ/テストデータ分離

学習データでテストしたらチートなので、分離しておきます。
80%を学習データ、残りの20%がテストデータです。

from sklearn.cross_validation import train_test_split
(x_train, x_test, y_train, y_test) = train_test_split(x, y, test_size=0.2, random_state=11)

学習

拾ってきた画像認識のレイヤで、モデルを構築しました。

import keras

from keras.layers.convolutional import Convolution2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Dense
from keras.layers.core import Dropout
from keras.layers.core import Flatten
from keras.models import Sequential
from keras.optimizers import SGD
from keras.callbacks import EarlyStopping
from keras.callbacks import LearningRateScheduler

nb_row = 3
nb_column = 3

model = Sequential()
model.reset_states()

model.add(Convolution2D(32, nb_row, nb_column, border_mode='same', input_shape=x_train.shape[1:]))
model.add(Activation('relu'))
model.add(Convolution2D(32, nb_row, nb_column))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Convolution2D(64, nb_row, nb_column, border_mode='same'))
model.add(Activation('relu'))
model.add(Convolution2D(64, nb_row, nb_column))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(2))
model.add(Activation('softmax'))
# model.summary()

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

history = model.fit(x_train, y_train, batch_size=32, nb_epoch=300, shuffle=True, verbose=False)

学習過程の可視化

上記のコードで学習した過程が、historyに格納されているのでmatplotlibで可視化。
なんだか学習できてるっぽい!!!
なんか最後らへんちょこっとロスが上がった瞬間あったけど、うぉぉぉぉぉぉぉ!!!!!!!!

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(history.epoch, history.history['acc'], color='red')
ax.plot(history.epoch, history.history['loss'], color='blue')
ax.legend(['acc','loss'], loc='center right')
ax.set_xlabel('epoch')
plt.show()

f:id:uphy:20161209220733p:plain

評価

テストデータを使って評価します!
精度90.24%超え、すげーーー!!!!!

が、lossは高い、どういうこっちゃー

score = model.evaluate(x_test, y_test, verbose=True)
print("%s: %.2f%%" % (model.metrics_names[1], score[1] * 100))
print("%s: %.2f%%" % (model.metrics_names[0], score[0] * 100))
41/41 [==============================] - 0s     
acc: 90.24%
loss: 65.00%

学習データ保存

今回は30分でしたが、時間をかけて学んだ大事な学習データなので、保存しておきます。

model.save('chun_model.h5')

学習データ読み込み

以下で読み込めます。

from keras.models import load_model

model = load_model("chun_model.h5")

精度は・・・?

以下のようなプログラムで、保存したモデルを読み込み、カメラから画像を取得し、リアルタイムにちゅん顔認識する。

0〜1の値で、ちゅん顔率を出す。1がちゅん顔、0が非ちゅん顔。

出来たのか出来てないのか・・・ 明るいところで正面向くと、だいたいちゅん判定される。

学習したちゅん顔写真はだいたいモデルのもので、明るい画像が多かったので、ちゅん顔を学習できてない気がする。残念・・・。

import time
import cv2
import numpy as np
from keras.models import load_model 
from face_detector import detect_faces, extract_rectangle

__model = load_model('chun_model.h5')

def recognize(image):
    faces = detect_faces(image)
    face_images = []
    points = []
    for face in faces:
        face_image = extract_rectangle(image, face['bounds'])
        face_image = cv2.resize(face_image, (64, 64))
        face_image = face_image / 255.
        x = np.array([face_image])
        y = __model.predict(x)
        face_images.append(face_image * 255)
        points.append(y[0][1])
    return face_images, points

if __name__ == '__main__':
    video_capture = cv2.VideoCapture(0)
    video_capture.set(3, 640)
    video_capture.set(4, 480)
    try:
        for i in range(20):
            ret, frame = video_capture.read()
            f, p = recognize(frame)
            for point in p:
                print(point)
            time.sleep(1)
    finally:
        video_capture.release()

あの日のちゅん顔

とりあえずいくつかピックアップして実行してみたけど、自分含めみんなだめだめ!

時間切れなので、評価があんまりできなかった・・・
後日更新するかも!

今後

データ数がすくないのはある程度仕方がないので、口だけ、目だけとか場所を絞って、明るさ等も調整することで、ちゅん顔と非ちゅん顔を適切に認識させたい!まだ頑張るかも!