機械学習で乃木坂46を顏分類してみた

こんなことをしてみたい
f:id:shintarom4869:20171217150543p:plain
↑これがしたい

pythonによる機械学習の勉強をしたので、実践ということで、人気アイドル「乃木坂46」の個人的に好きな5人のメンバーを区別して見ました。大きな流れはこんな感じです。

  1. web上から五人の画像を100枚ずつ取ってくる
  2. 画像から顔部分を取り出して保存、テストデータの取り出し
  3. 画像の水増し
  4. モデルを定義して、学習
  5. テスト(顔を四角く囲って、その人の名前を出力)

説明はこんなもんにして、彼女らの可愛さについて語りたいところですが、そういうブログではないので、少し技術的なことを書きます。
今回はjupyterを使って作業を進めました。notebook形式なので結果が見やすく初心者にはいい環境でした。環境は以下。

  • macOS:10.13.1
  • python:3.6.1
  • openCV:3.3.0
  • keras:2.1.2

まずはじめに、カレントディテクトリ上に、origin_image,face_image,face_scratch_image,test_imageディテクトリを追加しました。


1,Google APIを用いた画像取得

これを取得することで、Googleが提供するリソースにアクセスすることができます。今回でいえば、例えば「西野七瀬」と画像検索した時の上位100枚のURLを取得できます。APIのコードについてはこちらのブログを参考にしました。

Google Custom Search APIを使って画像収集 – Qiita

以下のコードでは、keywordsリスト[“生田絵梨花”,”齋藤飛鳥”,”白石麻衣”,”西野七瀬”,”橋本奈々未”]で画像検索、URLを取得後、画像に変換し、jpgファイルとしてorigin_imageディテクトリに保存という流れになっています。100枚以上取れればいいんですが、600枚くらいで403Error Forbiddenが出てしまいました。また、1検索あたりの最大取得URLは100枚のようです。101枚以上を指定するとHTTP Error 400 – Bad Requestが出てしまいました。

[python] 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)
[/python]

2,画像から顔部分を取り出し

まずopencvに搭載されているカスケード分類器を利用して、顔を検出します。今回は正面顔を検出するhaarcascade_frontalface_alt.xmlという分類器を使いました。引数のminNeighborsの値が重要でした。以下のコードでは、検出した顔部分をトリミングし、全て64×64ピクセルにリサイズして(学習器に入れやすくなる為)、jpg形式でface_imageディテクトリに保存ということをしてます。実際ここで、100内60~70枚ほどに減ってしまいます。その理由として、正面顔しか検出しないため、正面を向いていても傾いた顔は検出できないため、などが挙げられます。ここで唯一、自力でデータを操作しなければなりません。検出できなかったものは自分の目でチェックして自力でトリミングした方がいいデータができると思いますし、またツーショットなどは別の人の顔も一緒に検出してまったので、そういうのはラベルを変えるor削除しましょう。僕の場合は、この作業をしている時が一番こうf…幸せでした。またこのタイミングででテストデータを取り出しました。
カスケード分類器に関してはこのブログがおすすめです。

python+OpenCVで顔認識をやってみる – Qiita

f:id:shintarom4869:20171217150344p:plain

 

オリジナル写真
[python] 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 = "./origin_image/*"
out_dir = "./face_image"
in_jpg=glob.glob(in_dir)
in_fileName=os.listdir("./origin_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)
continue
image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cascade = cv2.CascadeClassifier("/usr/local/opt/opencv/share/OpenCV/haarcascades/haarcascade_frontalface_alt.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")
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ディテクトリに入れる
random.shuffle(in_jpg)
import shutil
for i in range(len(in_jpg)//5):
shutil.move(str(in_jpg[i]), "./test_image")
[/python]

3,画像の水増し

顔検出してテストデータを抜き取って残った数が、それぞれ50~70枚ほどになってしまい、これでは学習データとして不足するだろうと思ったので、左右反転、閾値処理、ぼかしを使って水増しを行います。これで学習データが8倍になります。(それでも少ない気がしますが。。)。画像の水増しは自動でやってくれるツールがありますが、自分でかけるコードは自分で書きたいので(キリッ)。このドキュメントは、画像の前処理を行う上で、もっともスタンダードな方法の一つです。これを知ってれば早いです。
画像の前処理 – Keras Documentation
ImageDataGenerator関数はリアルタイムにデータ拡張しながら,テンソル画像データのバッチを生成します。

以下のコードでは水増し加工をしたものをface_scratch_image1ディテクトリに保存してます。また、学習器に入れるために画像リストXと正解ラベルリストyを用意して、それぞれ追加していきます。

[python] #左右反転、閾値処理、ぼかしで8倍の水増し
import os
import numpy as np
import matplotlib.pyplot as plt
import cv2
def scratch_image(img, flip=True, thr=True, filt=True):
# 水増しの手法を配列にまとめる
methods = [flip, thr, filt] # ぼかしに使うフィルターの作成
filter1 = np.ones((3, 3))
# オリジナルの画像データを配列に格納
images = [img] # 手法に用いる関数
scratch = np.array([
lambda x: cv2.flip(x, 1),
lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1],
lambda x: cv2.GaussianBlur(x, (5, 5), 0),
])
# 加工した画像を元と合わせて水増し
doubling_images = lambda f, imag: np.r_[imag, [f(i) for i in imag]] for func in scratch[methods]:
images = doubling_images(func, images)
return images
# 画像の読み込み
in_dir = "./face_image/*"
in_jpg=glob.glob(in_dir)
img_file_name_list=os.listdir("./face_image/")
for i in range(len(in_jpg)):
print(str(in_jpg[i]))
img = cv2.imread(str(in_jpg[i]))
scratch_face_images = scratch_image(img)
for num, im in enumerate(scratch_face_images):
fn, ext = os.path.splitext(img_file_name_list[i])
file_name=os.path.join("./face_scratch_image"[f:id:shintarom4869:20171220032312p:plain],str(fn+"."+str(num)+".jpg"))
cv2.imwrite(str(file_name) ,im)</pre>
<pre class="code" data-lang="" data-unlink=""># 画像と正解ラベルをリストにする
import random
from keras.utils.np_utils import to_categorical
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
img_file_name_list=os.listdir("./face_scratch_image/")
print(len(img_file_name_list))
for i in range(len(img_file_name_list)):
n=os.path.join("./face_scratch_image",img_file_name_list[i])
img = cv2.imread(n)
if isinstance(img,type(None)) == True:
img_file_name_list.pop(i)
continue
print(len(img_file_name_list))
X_train=[] y_train=[] for j in range(0,len(img_file_name_list)-1):
n=os.path.join("./face_scratch_image/",img_file_name_list[j])
img = cv2.imread(n)
b,g,r = cv2.split(img)
img = cv2.merge([r,g,b])
X_train.append(img)
n=img_file_name_list[j] y_train=np.append(y_train,int(n[0:2])).reshape(j+1,1)
X_train=np.array(X_train)
img_file_name_list=os.listdir("./test_image1/")
print(len(img_file_name_list))
for i in range(len(img_file_name_list)):
n=os.path.join("./test_image1",img_file_name_list[i])
img = cv2.imread(n)
if isinstance(img,type(None)) == True:
img_file_name_list.pop(i)
continue
print(len(img_file_name_list))
X_test=[] y_test=[] for j in range(0,len(img_file_name_list)):
n=os.path.join("./test_image1",img_file_name_list[j])
img = cv2.imread(n)
b,g,r = cv2.split(img)
img = cv2.merge([r,g,b])
X_test.append(img)
n=img_file_name_list[j] y_test=np.append(y_test,int(n[0:2])).reshape(j+1,1)
X_test=np.array(X_test)
[/python]

4,モデルを定義して、学習

さて、一人当たり400~600枚ほどのデータを取得できました。いよいよ本番です。これを学習させる学習器を構築して、学習させます。水増しした練習データは合計で1832枚、テストデータ57枚です。
これ以降のディープニューラルネットワークのモデルの作成・学習はKerasというライブラリを使用しました。KarasはTheanoやTensorFlowといった機械学習のライブラリのラッパーです。さて、学習器の構造ですが、CIFAR-10のサンプルを参考に作って見ました。それと、keras documentationは日本語のドキュメントなのでわかりやすかったです。畳み込み層、プーリング層の数を増やしたり、epoch数-精度のグラフをみて、精度の高かったものがこちらです。またこちらのブログも参考になりました。
TensorFlowによるももクロメンバー顔認識(前編) – Qiita

ディープラーニングでザッカーバーグの顔を識別するAIを作る①(学習データ準備編) – Qiita

[python] – 入力 (64×64 3chカラー)
– 畳み込み層1
– プーリング層1
– 畳み込み層2
– プーリング層2
– 畳み込み層3
– プーリング層3
– 平坦層1
– 全結合層1(sigmoid関数)
– 全結合層2(sigmoid関数)
– 全結合層3(softmax関数 出力5)
[/python]

epochs数を60にしてテストデータの精度は70%になりました。低いですね。理由として、みんな可愛いから特徴量が低い、取得データ不足などが挙げられます。全結合層の関数は一般的にはrelu関数の方が良いとされますが、試して見たところ今回はsigmoid関数の方が精度が高かったです。浅めの学習モデルだとsigmoid関数の方がいいのかもしれません。

epochs-accグラフはこちらです。

f:id:shintarom4869:20171220032312p:plain

 

横軸:epoch数,縦軸:精度
[python] from keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D
from keras.models import Sequential, load_model
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
#plt.imshow(X_train[0])
#plt.show()
#print(y_train[0])
# モデルの定義
model = Sequential()
model.add(Conv2D(input_shape=(64, 64, 3), filters=32,kernel_size=(2, 2), strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(2, 2), strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(2, 2), strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation("sigmoid"))
model.add(Dense(128))
model.add(Activation(‘sigmoid’))
model.add(Dense(5))
model.add(Activation(‘softmax’))
# コンパイル
model.compile(optimizer=’sgd’, loss=’categorical_crossentropy’,metrics=[‘accuracy’])
# 学習
# model.fit(X_train, y_train, batch_size=32, epochs=50)
#グラフ用
history = model.fit(X_train, y_train, batch_size=32, epochs=100, verbose=1, validation_data=(X_test, y_test))
# 汎化制度の評価・表示
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print(‘validation loss:{0[0]}\nvalidation accuracy:{0[1]}’.format(score))
#acc, val_accのプロット
plt.plot(history.history["acc"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_acc"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
#モデルを保存
model.save("my_model.h5")
[/python]

5,テスト

見分けるモデルが完成したので、これを使ってアイドルの顔を見分けていこうと思います。画面に文字を追加したり、四角く囲ったりするのもopencvを用いました。いろんな画像でテストして見ると、生田絵梨花と誤認識する確率が高いように思いました。

コードは、画像中の顔を検出、四角くくくる、モデルで測定、名前を記入、といった感じです。

[python] import numpy as np
import matplotlib.pyplot as plt
def detect_face(image):
print(image.shape)
#opencvを使って顔抽出
image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cascade = cv2.CascadeClassifier("/usr/local/opt/opencv/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml")
# 顔認識の実行
face_list=cascade.detectMultiScale(image_gs, scaleFactor=1.1, minNeighbors=2,minSize=(64,64))
#顔が1つ以上検出された時
if len(face_list) &gt; 0:
for rect in face_list:
x,y,width,height=rect
cv2.rectangle(image, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (255, 0, 0), thickness=3)
img = image[rect[1]:rect[1]+rect[3],rect[0]:rect[0]+rect[2]] if image.shape[0]&lt;64:
print("too small")
continue
img = cv2.resize(image,(64,64))
img=np.expand_dims(img,axis=0)
name = detect_who(img)
cv2.putText(image,name,(x,y+height+20),cv2.FONT_HERSHEY_DUPLEX,1,(255,0,0),2)
#顔が検出されなかった時
else:
print("no face")
return image
def detect_who(img):
#予測
name=""
print(model.predict(img))
nameNumLabel=np.argmax(model.predict(img))
if nameNumLabel== 0:
name="Ikuta Erika"
elif nameNumLabel==1:
name="Saito Asuka"
elif nameNumLabel==2:
name="Shiraishi Mai"
elif nameNumLabel==3:
name="Nishino Nanase"
elif nameNumLabel==4:
name="Hashimoto Nanami"
return name
# model = load_model(‘./my_model.h5’)
image=cv2.imread("./origin_image/01.0.jpg")
if image is None:
print("Not open:")
b,g,r = cv2.split(image)
image = cv2.merge([r,g,b])
whoImage=detect_face(image)
plt.imshow(whoImage)
plt.show()
[/python]
最後にちょっとだけ感想

今回のコードの制作期間は二日くらいでしたが、その6,7割くらいの時間を素材集めに使ってしまいまして、機械学習についてもっと触れたかったなと。。。でも、細かい文法などの知識が固まったかなあ。もっと人数の増やした、良い学習モデルを作ってサービスとしてリリースしたいなと思いました。また、さらなる上達のために、今回の応用として転移学習にも挑戦していきたいです。

ラストに結果を載せていきます。失敗込みです。(汗)

f:id:shintarom4869:20171217213221p:plainf:id:shintarom4869:20171217150444p:plainf:id:shintarom4869:20171217150441p:plainf:id:shintarom4869:20171217150403p:plainf:id:shintarom4869:20171217150443p:plainf:id:shintarom4869:20171217150445p:plain

 

テスト
追記

モデル学習のテストデータに水増しされた写真も入っていたことを指摘いただきまして、修正しました。テストデータに水増しされたデータを使うと、テストデータの精度は高くなりますが、オリジナルのデータに弱いモデルとなります。その他細かいコードの手直しをしました。

4 Comments

xanxus1204

はじめまして!
私も最近機械学習を勉強していて、乃木坂も好きなのでこの手のことをやってみたいと思っていました。
ただ、やはり画像を集める部分がネックになっていて手をつけられずにいました。
とても参考になる記事です!ありがとうございます!

fnakamurax

はじめまして。
私も最近 Deep Learning を学習し (はじめ) ています。
また、乃木坂46も好きなので、楽しく記事を拝見させていただきました。
一点お聞きしたいのですが、
Data Augmentation をした後のデータセットを train/test split するというのは、一般的なのでしょうか。
あらかじめ train/test split をして test データセットをホールドアウトしておいてから、train データセットについてのみ augumentation を施すと思っていましたので、少し気になり、お聞きしたいと思いました。

shintarom4869

ブログを書いたものです。すでに追記させていただきましたが、Data Augmentation をした後のデータセットを train/test split するというのは基本的にないそうです。ご指摘いただいた通り、Data Augmentationをする前にテストデータを別ファイルに抜き取りました。

hayataka2049

はじめまして
私はここでこの記事を知りました。
https://teratail.com/questions/123872
画像の読み込み以下のコードに、
f:id:shintarom4869:2017….というfotolife記法が混入しています。
色々な混乱を招くと思うので、このコメントに気づき次第、修正をお願いします。

現在コメントは受け付けておりません。