はじめに
この記事では、写真に写っている人物の顔をディープラーニングによって分類するアプリの開発体験について紹介します。このアプリでは、写真の人物の顔が、いわゆる「かわいい」かを判定します。アプリ自身はラズパイのWebサーバで稼働し、写真画像を送ると、写真に写った人物がかわいいか判定して結果を返します。
開発にきっかけは、人間が他人の顔を「かわいい」と判断する基準は非常に曖昧なので、それでは機械に判断してもらおう、というものです。#とはいえ、結局はどんな画像をかわいい顔の画像データとして集めたかに依存するので、ここに開発者の判断が入ってしまうのですが。。
環境
今回は、教師データから分類モデルを作成する学習環境としてパソコンを利用し、新たな写真画像に対する推論環境としてラズパイを利用しました。下記に私の環境の詳細を記載します。
- 学習用環境(パソコン)
- Python 3.6.4
- Keras 2.1.4
- Tensorflow 1.4.0
- OpenCV 3.4.0.12
- 推論用環境(ラズパイ)
- Raspbian 8 (jessie)
- Python 3.4.2
- Keras 2.1.4
- Tensorflow 1.1.0
- OpenCV 3.4.1-dev # devバージョンでなくても問題ありません
- Flask 0.12.2
今回OpenCVは顔画像の抽出のために利用しています。また、FlaskはWebサーバ用のフレームワークとして利用します。
なお、ラズパイにOpenCVをインストールするには、現在のところコンパイルする方法しかありませんでした。下記記事等を参考にしてインストールしてください。
ラズパイにOpenCVをインストールする方法
データの準備
教師データとして、かわいい顔の人物画像と、それ以外の画像とをそれぞれ用意する必要があります。私の場合はGoogleで「可愛い顔画像」などをキーワードに人物の顔を画像検索して入手したデータを使いました。できれば数百枚以上は画像を集めた方が良いと思いますが、私は数十枚程度集めて力尽きました。
全体の流れ
学習と推論の流れを大雑把に書くと、下記の手順になります
(1)学習用環境で教師データを用いて分類モデルを生成
(2)生成した分類モデルを保存し推論用環境にコピー
(3)推論用環境で画像に対して分類を実施
以下では、それぞれの内容について説明していきます。
(1)学習用環境で教師データを用いて分類モデルを作成
学習用環境では、準備した画像データを教師データとして、KerasとTensorflowを用いて分類モデルを生成します。準備したデータは、それぞれ「かわいい人物画像」と「それ以外の画像」に分けて、別々のディレクトリに保しておきます。
早速、学習用環境のコードを見ていきます。コード全体はgithubにありますので、そちらを参照してください。
ディレクトリから画像データをロード
# load images for train
# - for target images
for picture in list_pictures(target_dir, ext='jpg'): #拡張子'jpg'ファイルを指定しています
img = img_to_array(load_face_img(picture, target_size=(IMAGE_SIZE, IMAGE_SIZE)))
x.append(img)
y.append(1) # 正解ラベル
ここでは、keras.preprocessing.imageモジュールのlist_picturesを使って、ディレクトリ内の全ての画像ファイルを取り込んでいます。ここでtarget_dirはかわいい人物画像を保存したディレクトリを指定しています。なお、私の使った画像ファイルの拡張子がjpgでしたので、引数にext='jpg'を指定しています。
load_face_imageで実際にpictureに指定されたファイルを読み込んでいます。詳細は次に説明します。
画像データから顔画像を抽出
def load_face_img(file, target_size):
# 画像ファイル読み込み
img = cv2.imread(file)
# グレースケール変換
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 顔領域の探索
face = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=3, minSize=(60, 60))
# 検出部分の切り出し。最初に検出した1つだけを切り出す(1画像に顔1つが前提)
if len(face):
x, y, w, h = face[0]
face_img = img[y:y+h, x:x+w]
else:
face_img = img
# サイズの変更
face_img = cv2.resize(face_img, target_size)
return np.asarray(face_img)
ここでopenCVを利用して、画像ファイルの読み込み、顔領域の抽出、サイズの変更を実施しています。いったん画像をグレースケール画像greyに変換していますが、これは顔画像検出のための前処理です。実際に抽出する顔領域の画像は、元のカラー画像imgを用いていることに注意してください。基本的には1つの画像には顔は1つだけであることを前提にしています。もし画像に複数の顔があっても、最初に検出した顔だけを利用するようにしています。なお、今回の顔検出にはHaar Cascade識別器を利用しています。OpenCVでの顔検出の詳しい情報は下記サイトを参照してください。
Python版OpenCVで顔検出(Haar Cascade)
分類性能の向上のためのデータ拡張
いよいよ学習を実行!と言いたいところですが、ここで1点工夫が必要です。ここまでで述べた内容で、実際に分類モデルを作成して評価したところ、下記のような傾向が見られました。
- 明るい場所で撮影した画像、ピントが合っている画像は「かわいい」と判別される傾向がある
- 少し薄暗い暗い、ぼやけた画像は「かわいい」と判定されることが少ない
上記は少し考えれば明らかでした。私の収集した「かわいい人物画像」は、だいたいはプロのカメラマンが明るく美しい写真を撮影しているのに対して、「それ以外」の画像は、自撮り画像等、撮影環境が悪い場合が明らかに多いためです。このため、こういった撮影環境の違いの影響を軽減して分類モデルを生成する必要があります。また、私が収集できた画像データの量が少なすぎるのも精度が悪くなる原因の1つです。
そこで、データ拡張(Data Augmentation)と呼ばれる手法を用います。これは、オリジナルの画像データに対する輝度の変更や、左右反転など、様々な加工を施したデータを生成して、教師データに追加して活用します。これで、撮影環境の影響軽減や、少ない画像数であってもデータが水増しされることで、分類モデルの精度向上が期待できます。今回はKerasのkeras.preprocessing.imageモジュールのImageDataGeneratorを利用し、データ拡張を実施しています。コードは下記になります。
# data augmentation
from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
zoom_range=0.3,
horizontal_flip=True,
shear_range=0.39, # pi/8
channel_shift_range=100,
samplewise_center=True,
data_format=K.image_data_format())
datagen.fit(x_train)
もう少し詳細が知りたい方は、下記のページにKerasのImageDataGeneratorを詳しく紹介していますので、参考にしてください。
分類モデルの生成
さて、いよいよ学習です。今回はCNNを用いていますが、ラズパイで推論することを考慮して、複雑なネットワークにはしていません。なお、私の収集した教師データの画像数が数十枚程度でしたので、バッチサイズbatchsizeは非常に小さい値となっています。こちらは実際に活用されるデータ量に合わせて変更した方が良いと思います。また、今回のコードはtrain_test_splitでテストデータを生成できるようにしていますが、引数がtest_size=0.0となっていますので、テストデータは生成されません。もし分類結果が良くない場合などは、ここを変更してテストデータを生成し、ハイパーパラメータのチューニングを実施してみてください。
# テストデータ作成(下記ではテストデータ未作成)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.0, random_state=211)
# ハイパーパラメータ
batch_size = 10
epochs = 50
learning_rate = 1e-5
model = Sequential()
model.add(Conv2D(32, kernel_size=(5, 5),
activation='relu',
input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (5, 5), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(256, activation='relu'))
model.add(Dense(NUM_CLASSES, activation='softmax'))
#display model summary
model.summary()
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs,
verbose=1, validation_data=(x_test, y_test))
(2)生成した分類モデルを保存し推論用環境にコピー
学習の結果、生成した分類モデルをファイルに保存します。実際にはモデルと重みの両方を保存する必要があります。保存されたモデルと重みのファイルは、ラズパイでの推論用にコピーしておきます。重みのファイルそれなりに大きなファイルサイズ(私の場合400MB超)になりますので、ラズパイに十分なストレージ容量があるかご注意ください。
# モデルの保存
model_json = model.to_json()
open('model.json', 'w').write(model_json) # モデル
model.save_weights('model.h5'); # 重み
(3)推論用環境で新しい画像に対して分類を実施
ここからは推論側になりますので、ラズパイで実行する内容になります。推論用環境ではFlaskを用いてWebサーバを起動し、クライアントから画像ファイルが送信されると、学習側で生成した分類モデルを用いて画像を判定し、結果を返します。
推論用環境には、推論用コードとともに、templatesディレクトリを作成してFlaskでクライアントに表示するページの内容(ここではindex.html)を準備しておく必要があります。また、クライアントから送信された画像ファイルを保存しておくディレクトリとして、staticディレクトリを作成しておきます。下記がディレクトとファイルの構造の概要です。
- ./inference.py -- 推論用コード
- ./templates/index.html -- Flask用のtemplate
- ./static/ -- クライアントから送信された画像ファイルを保存
index.htmlの内容は下記になります。filepathに値がある場合のみ画像と結果を表示するため、画像の送信用の画面と、結果表示用の画面の両方でこの内容を利用できます。
<html>
<body>
{% if filepath %}
<IMG SRC="{{filepath}} " width="128" height="128" BORDER="1"> predict:{{predict}} probability:{{proba}}% <BR>
<HR>
{% endif %}
ファイルを選択して送信してください<BR>
<form action = "./" method = "POST"
enctype = "multipart/form-data">
<input type = "file" name = "file" />
<input type = "submit"/>
</form>
</body>
</html>
推論用コードでは、クライアントからのGETリクエスト時には画像送信用のページを返します。PUTリクエストとしてクライアントから画像が送信されると、画像から顔画像を抽出します。この部分は、学習側でのコード内容とほぼ同様です。
その後、分類モデルのファイルを読み込んで、抽出された顔画像に対して推論を実行し、結果を返します。下記にその部分のコードを示します。model_from_jsonやmodel.load_weightsで分類モデルのファイルを読み込んだ後、推論結果として分類結果と確率値をそれぞれpredict、probaとして、index.htmlのパラメタとして返します。
#モデルを読み込む
model = model_from_json(open('model.json').read())
# 重みを読み込む
model.load_weights('model.h5')
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adam(),
metrics=['accuracy'])
# prediction
predict = model.predict_classes(x_val)
proba = model.predict_proba(x_val)[0][1]*100 # probability of class 1
return render_template('index.html', filepath = filename , predict = predict, proba = proba )
推論用のコードやFlaskの使い方は、下記のページの内容を大いに参考にさせていただきました。
ChainerとFlaskで作る機械学習デモアプリ 後編 Webアプリの構築
クライアントからの接続
推論用環境(ラズパイ)で推論用コードを起動してWebサーバを起動し、クライアントから接続します。スマホなどのブラウザから、推論用環境のIPアドレスにFlaskのデフォルトのポート番号である5000を指定して接続します。
結果
さて、結果ですが、私が「かわいい」と期待する私の娘の写真と、「それ以外」になるべき私の写真とを使って評価して見ました。結果は下記になります。
娘の写真での判定結果: 無事「かわいい」(predict:[1])と判定!
私の写真での判定結果: 無事「それ以外」(predict:[0])と判定
最後に
なんとか一通り動くものができたので良かったと思っています。ただ、実際にやってみると、やはりデータ収集の大変さや、さらなる改良(例えば男女の分類を追加したい、画像の背景を除去したい)など、気になる点は多数あります。また、私のラズパイ(Raspberry Pi3)では、画像を送ってから結果が返ってくるまで7秒ほどかかりました。これも改善の余地があると思います。
なお、今回の記事では、「かわいい」と「それ以外」の分類として紹介しましたが、教師データの集め方によって、他の画像分離用途にも応用できる内容となっています。
本記事の内容が皆様の参考になれば幸いです。