Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

機械学習で乃木坂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

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

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

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

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

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_image1",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)

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

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

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

f:id:shintarom4869:20171217150344p:plain
オリジナル写真

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_image1/*"
out_dir = "./face_image1"
in_jpg=glob.glob(in_dir)
in_fileName=os.listdir("./origin_image1/")
# 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)

3,画像の水増し

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

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

#左右反転、閾値処理、ぼかしで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]
    # 画像のサイズを習得、ぼかしに使うフィルターの作成
    img_size = img.shape
    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_image1/*"
in_jpg=glob.glob(in_dir)
img_file_name_list=os.listdir("./face_image1/")
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_image1",str(fn+"."+str(num)+".jpg"))
        cv2.imwrite(str(file_name) ,im)

# 画像と正解ラベルをリストにする
import random
from keras.utils.np_utils import to_categorical

img_file_name_list=os.listdir("./face_scratch_image1/")
print(len(img_file_name_list))

for i in range(len(img_file_name_list)-1):
    n=os.path.join("./face_scratch_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))

random.shuffle(img_file_name_list)
X=[]
y=[]

for j in range(0,len(img_file_name_list)):
    n=os.path.join("./face_scratch_image1",img_file_name_list[j])
    img = cv2.imread(n)
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    X.append(img)
    n=img_file_name_list[j]
    y=np.append(y,int(n[0:2])).reshape(j+1,1)
print(y.shape)
print(y[0])
X=np.array(X)
print(X.shape)
#確認用
# plt.imshow(X[0])
# plt.show()

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

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

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

- 入力 (64x64 3chカラー)
- 畳み込み層1
- プーリング層1
- 畳み込み層2
- プーリング層2
- 畳み込み層3
- プーリング層3
- 平坦層1
- 全結合層1(sigmoid関数)
- 全結合層2(sigmoid関数)
- 全結合層3(softmax関数 出力5)

epochs数を50にしてテストデータの精度は92%になりました。高いですね。みんな可愛いから特徴量が低いと思ったのですが、案外いい結果に驚きました。全結合層の関数は一般的にはrelu関数の方が良いとされますが、試して見たところ今回はsigmoid関数の方が精度が高かったです。浅めの学習モデルだとsigmoid関数の方がいいのかもしれません。

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

f:id:shintarom4869:20171217175012p:plain
横軸:epoch数,縦軸:精度

from keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D
from keras.models import Sequential, load_model

#練習データとテストデータに分ける
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8)

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")

5,テスト

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

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

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) > 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]<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("/Users/shintaro/aidemy/sample/origin_image1/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()
最後にちょっとだけ感想

今回のコードの制作期間は二日くらいでしたが、その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
テスト