Aidemy Tech Blog

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

TinderAPIで女の子の顔写真を集めて、加工アリorナシを自動で判定してみた

こんにちは、Aidemy研修生のタットヤムです。

みなさん、かわいい女の子は好きですか?僕は大好きです。

     f:id:kirraria:20180628002323p:plain

今どきはマッチングアプリでいつでもかわいい女の子を見放題なのでいい時代ですね。中でも「Tinder」はアメリカを中心に世界的に流行ってる、相手の写真を見てアリなら右、ナシなら左にスワイプするだけでマッチングするという、かわいい子の顔を見て癒されるのにうってつけなアプリです。

しかしTinderでスワイプして写真を眺めていると、「この子かわいいけど加工してるっぽい?」なんてことありますよね。

Aidemyの研修でせっかくディープラーニングを学んだので、今回はTinderのPythonAPIを使って日本各地の女の子の顔写真を集めて、ディープラーニングで加工がされているかどうかを自動で判定してみようと思います。

だいたい手順はこんな感じになります。そのまま目次にしてしまいますね。

 

0.環境

主な使用ライブラリなどを載せておきます。

・Python 3.6.4    プログラミング言語

・Anaconda 1.6.9    プログラミングする環境を管理する

・Jupyter 4.4.0           ノートにそのまま書くみたいにプログラミングができる

・numpy 1.14.0          数学でやった行列みたいなのを上手に扱える

・matplotlib 2.1.2       グラフを描いたりしてくれる

・OpenCV 3.4.1.15    写真を上手に扱える

・Keras 2.2.0             機械学習のプログラムがたくさん入ったパッケージ的なもの

・Pynder 0.0.13    TinderをPythonで動かすためのAPI

 

インポート部分は全部でこんな感じになってます。

import os
import re
import random
import matplotlib.pyplot as plt
import cv2
import numpy as np
import urllib.request
from urllib.parse import quote
import httplib2
import pynder
from keras import optimizers
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, Dropout, Flatten, Input
from keras.models import Model, Sequential, load_model
from keras.utils.np_utils import to_categorical

 

加えていくつか関数を定義しておきます。

def get_image(img_url): # ウェブ上の画像をダウンロードする
    opener = urllib.request.build_opener()
    http = httplib2.Http(".cache")
    response, content = http.request(img_url)            
    return content

def aidemy_imshow(name, img): # Jupyter notebook上で画像を表示
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    plt.imshow(img)
    plt.show()
cv2.imshow = aidemy_imshow

def jpg_count(folder_name): # ディレクトリ内のjpgファイルを数える
    files = os.listdir("./"+folder_name)
    jpgcount = 0
    for file in files:
        index = re.search(".jpg", file)
        if index:
            jpgcount += 1
    return jpgcount

def image_save(img,folder_name): # 画像を保存する
    # ディレクトリがなければ作る
    if not os.path.exists(folder_name):
        os.mkdir(folder_name)
    jpgcount = jpg_count(folder_name)
    # カウント数+1でファイル名をつけて保存
    w_pass = "./{}/{}.jpg".format(folder_name,jpgcount)
    cv2.imwrite(w_pass,img)

1.写真を集める

はじめに、Tinderから写真をたくさん集めます。コードは各章の下に貼っておきます。

PynderでTinderにアクセスするためには、アクセストークンというものを調べないといけません。アクセストークンはこのWebページ(Tinderface)に書いてあるとおりにやると調べることができます。

なおディープラーニングの学習に使う写真は、札幌・秋田・宇都宮・東京・横浜・名古屋・京都・大阪・福岡・沖縄を回るようにして集めました。

こんな感じで各都市で300人ずつスワイプして画像を集めました。 

f:id:kirraria:20180703203212p:plain

# 写真を保存するディレクトリを作成する
if not os.path.exists("images"):
        os.mkdir("images")

AccessToken = "ここにアクセストークンを貼ってね"
session = pynder.Session(AccessToken)
friends = session.get_fb_friends()

# 探索する都市の緯度と経度を指定
Locations = [["Sapporo", 43.055248, 141.345505],
             ["Akita", 40.824589, 140.755203],
             ["Utunomiya", 36.554241, 139.897705],
             ["Tokyo", 35.680909, 139.767372],
             ["Yokohama", 35.465786, 139.622313],
             ["Nagoya", 35.154919, 136.920593],
             ["Kyoto", 35.009129, 135.754807],
             ["Osaka", 34.702509, 135.496505],
             ["Hukuoka", 33.579788, 130.402405],
             ["Okinawa", 26.204830, 127.692398]
            ]

# 各都市で取得する人数の上限
Limit = 300

for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for url in photo_urls:
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            image_save(img,"./images/{}".format(location[0]))
    print(location[0] + " is done")

2.顔の部分を抽出する

次に、集めた写真の顔の部分だけを抽出して、ディープラーニングが学習しやすいようにします。しかし何千枚もの写真を1つ1つ加工していくのは大変なので、ここではHaar-like特徴分類器というプログラムを使います。

仕組みを簡単に説明すると、人の顔はだいたい両目のところが暗く鼻のところは明るいとか、目が暗くてその下の頬は明るいとか、明暗でいくつかパターンがあって、そのパターンがあるものを人の顔として認識するという仕組みです。詳しくは下の記事を読んでみてください。

qiita.com

 結果を見てみると、わりとしっかりと顔に反応して抽出してくれていることが分かります。ディープラーニングとはまた違いますが、一見単純な仕組みでもちゃんと人の顔を抽出してくれるっていうのが何というかロマンがありますよね。

ただし顔スタンプなど、顔に似ているものもけっこう拾ってしまうので、これは次の手順のときに、一緒に分別しないといけません。

f:id:kirraria:20180703203219p:plain

# HAAR分類器の特徴量
cascade_path = "D:\Anaconda3\envs\Conda_AITrade\Lib\site-packages\cv2\data\haarcascade_frontalface_alt.xml"

face_num = [0, 0] # [0]が顔認識数、[1]が顔認識されなかったファイル数(経過表示)

if not os.path.exists("faces"):
        os.mkdir("faces")

for location in Locations:
    # 都市ディレクトリ内のjpgファイル数を読み込み
    imageN = jpg_count("./images/{}".format(location[0]))
    print(location[0]," : ",imageN)
    
    # ディレクトリ内のファイルをリストに格納
    dir_pass = "./images/{}/".format(location[0])
    file_names = os.listdir(dir_pass)
    for file_name in file_names:
        print(file_name, face_num)
        # 画像を読み込み、顔認識分類器にかける
        img = cv2.imread(dir_pass + file_name)
        # 画像ファイルが読み込めないとき
        if img is None:
            print("img is none.")
            continue
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        cascade = cv2.CascadeClassifier(cascade_path)
        faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
        # 顔が検出されたとき
        if len(faces_poses) > 0:
            for fp in faces_poses:
                img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                img_face = cv2.resize(img_face, (64, 64))
                image_save(img_face,"faces/{}".format(location[0]))
                face_num[0] += 1
        # 顔が検出されなかったとき
        else:
            face_num[1] += 1

3.重複した顔写真を削除する

次に、集めた顔写真の中で重複しているものを削除します。

同じ顔写真が複数あるとディープラーニングの学習に偏りが生じてしまう可能性があるので、しっかり消していきましょう。

今回はある写真が複数あるかどうかを調べるために、写真のヒストグラムの類似率を計算しました。カラー画像を構成する3色それぞれの濃淡をヒストグラムとして計算し、ヒストグラムが一定以上類似している写真を重複として判定して削除します。

類似度の閾値は、画像の一覧を目視してだいたい重複がなくなるぐらいに設定しましたが、もうちょっといい設定方法があったかもしれません。

顔写真はこれまでの処理で削られた結果、全部で1294枚となりました。

# パラメータ定義 ####################################
# 類似度の閾値
criret = 0.999

# 処理の実行 #######################################
indir_pass = "./faces/"
outdir_pass = "./faces_dd"
file_names = os.listdir(indir_pass)
passhist_list = []

for file_name in file_names:
    # 画像を読み込む
    img = cv2.imread(indir_pass + file_name)
    # 画像ファイルが読み込めないとき
    if img is None:
        print("img is none.")
        continue
    # RGBのヒストグラムを計算しリストに格納
    img_hist = []
    img_hist.append(cv2.calcHist([img], [0], None, [256], [0, 256]))
    img_hist.append(cv2.calcHist([img], [1], None, [256], [0, 256]))
    img_hist.append(cv2.calcHist([img], [2], None, [256], [0, 256]))
    # 参照ヒストグラムとの類似率を計算し、閾値以下であれば新規顔画像として保存する
    for pass_hist in passhist_list:
        if cv2.compareHist(img_hist[0],pass_hist[0],cv2.HISTCMP_CORREL) > criret:
            break
        if cv2.compareHist(img_hist[1],pass_hist[1],cv2.HISTCMP_CORREL) > criret:
            break
        if cv2.compareHist(img_hist[2],pass_hist[2],cv2.HISTCMP_CORREL) > criret:
            break
    else: # すべての参照ヒストグラムとの類似率が閾値以下だったとき
        passhist_list.append(img_hist)
        image_save(img, outdir_pass)

4.顔写真を普通の写真と加工写真に分ける

次に、集めた顔写真を普通の写真と、プリクラの写真、SNOWで加工した写真の3つに分けます。

実はこの作業が一番大変で、何千の画像データを1つ1つ見て確認してディレクトリに振り分けていくのは気が遠くなるような作業でした。またプリクラならまだわかりやすいのですが、SNOWで加工した写真は一見普通に見えるようになっている写真もあるので判定に困ることも多かったです。

しかしここの振り分けのゆらぎが、のちのモデルの性能に大きな影響を与えることもあるので、しっかりと自分の中で軸を定めて分けていきましょう。

5.ディープラーニングモデルに学習させる

いよいよ振り分けた顔写真を使って、ディープラーニングの学習を行っていきます。ディープラーニングをするためには、学習モデルを設計しないといけないのでそれから始めましょう。

5.1.学習モデル

今回の学習モデルはこんな感じに設計しました。今回はVGG16モデルを転移学習として使用し、その後2層の全結合層を経て出力層にて3クラスの分類を行います。

- 入力 (64x64×3 : 64×64のカラー画像)
- VGG16モデル(16層の転移学習モデル)
- 全結合層1(64, sigmoid関数, dropout=0.5)
- 全結合層2(32, sigmoid関数, dropout=0.5)
- 出力層(3, softmax関数)

5.2.転移学習

ちなみに2行目にあるVGG16モデルというのは、転移学習と呼ばれるもので、簡単にいうとほかの人があらかじめ学習までさせてくれた学習モデルをそっくり借りてくるというものです。

人間が物体を認識するとき、たとえばこんな風になりますよね。

→映像が見えた
→なんか丸っこいものが見えた、顔かな
→なんか目が大きいからSNOWっぽい

この「映像が見えた」ところから「なんか目が大きいからSNOWっぽい」まですべての工程を学習させるのがしんどいので、「映像が見えた」から「なんか丸っこいものが見えた、顔かな」ってところまでをほかの人が学習させてくれたものを使い、最後の「なんか目が大きいからSNOWっぽい」で加工されているかどうか判定するところだけを自分のお好みで学習させるという感じです。

5.3.データの水増し

今回はデータの80%をトレーニングデータ、20%をテストデータとしました。この2つを分ける理由は後述します。

それとモデル学習の前にもう一つ、データの水増しというのを行います。トレーニングデータの顔写真1035枚だとちょっと少ないので、画像を左右反転させたり、ぼかしたりして一つの画像を何枚にも増やします。今回は一つの画像が2×2×2=8枚になるように水増ししたので、データは8280枚となりました。

5.4.学習の実行

最後にモデルの学習を実行します。そのまま学習を進めてしまうとVGG16モデルのパラメータも変わってしまうので、そこだけ変わらないように固定します。今回はミニバッチ=32のepoch=100で学習を実行しました。

学習の経過を下のグラフに示します。青線がトレーニングデータに対する正答率で赤線がテストデータに対する正答率です。

f:id:kirraria:20180703184325p:plain

結果としてトレーニングデータに対してテストデータの正答率の上がり方が悪いので、若干過学習気味といった感じになってしまいました。最終的な正答率は約90%でした。原因としては、ラベリング時の振り分け基準があいまい、使ったモデルが適当でなかった等が考えられます。

# フォルダを生成
if not os.path.exists("result"):
        os.mkdir("result")

# Pynderを使いTinderにログイン
session = pynder.Session(AccessToken)

# モデルの読み込み
model = load_model("my_model.h5")

for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for un, url in enumerate(photo_urls):
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            cascade = cv2.CascadeClassifier(cascade_path)
            faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
            # 顔が検出されたとき
            if len(faces_poses) > 0:
                for fp in faces_poses:
                    # 顔を囲む枠を生成
                    cv2.rectangle(img, tuple(fp[0:2]), tuple(fp[0:2]+fp[2:4]), (0, 0, 255), thickness=3)
                    # 学習したモデルでスコアを計算する
                    img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                    img_face = cv2.resize(img_face, (64, 64))
                    score = model.predict(np.expand_dims(img_face, axis=0))
                    # 最も高いスコアを書き込む
                    score_argmax = np.argmax(np.array(score[0]))
                    text =  "{0} {1:.3}% ".format(Labels[score_argmax], score[0][score_argmax]*100)
                    cv2.putText(img, text, (fp[0],fp[1]+fp[3]+30), cv2.FONT_HERSHEY_DUPLEX, 1, (0,0,255), 2)
                # 画面に表示
                cv2.imshow("{}_{}".format(location[0], count), img)
                plt.show()
                image_save(img,"result")
            # 顔が検出されなかったとき
            else:
                continue

6.実際にTinderの写真に適用してみる

それでは実際に、新規にTinderから取得した写真をモデルに入力し、正しくラベリングできているかどうかを見ていきましょう。

まずは普通の写真から、高いパーセンテージで普通とラベリングされています。

f:id:kirraria:20180703200241j:plain
f:id:kirraria:20180703200207j:plain

 

次にプリクラの写真です。左の写真はちょっとパーセンテージが低いですね。ほかの写真でも実行した結果、プリクラの写真はたまに普通の写真としてラベリングされてしまうようです。

f:id:kirraria:20180703200343j:plain
f:id:kirraria:20180703200425j:plain

 

最後にSNOWで加工された写真です。SNOWの写真もプリクラの写真と同様に、基本的には拾ってくれるのですが、たまに普通の写真としてラベリングされてしまいました。

f:id:kirraria:20180703200820j:plain
f:id:kirraria:20180703200842j:plain

 

これらは間違ってしまっているものです。ぼやけていたりノイズが入ったりしていると誤った判定になりやすいなと感じました。

f:id:kirraria:20180703202753j:plain
f:id:kirraria:20180703202805j:plain
# フォルダを生成
if not os.path.exists("result"):
        os.mkdir("result")

# Pynderを使いTinderにログイン
session = pynder.Session(AccessToken)

# モデルの読み込み
model = load_model("my_model.h5")


for location in Locations:
    session.update_location(location[1], location[2])
    users = session.nearby_users()
    count = 0
    for user in users:
        count += 1
        if count > Limit:
            break
        photo_urls = user.get_photos(width="640")
        for un, url in enumerate(photo_urls):
            img_buf = np.fromstring(get_image(url), dtype='uint8')
            img = cv2.imdecode(img_buf, 1)
            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            cascade = cv2.CascadeClassifier(cascade_path)
            faces_poses = cascade.detectMultiScale(img_gray, scaleFactor=1.1, minNeighbors=2, minSize=(64,64))
            # 顔が検出されたとき
            if len(faces_poses) > 0:
                for fp in faces_poses:
                    # 顔を囲む枠を生成
                    cv2.rectangle(img, tuple(fp[0:2]), tuple(fp[0:2]+fp[2:4]), (0, 0, 255), thickness=3)
                    # 学習したモデルでスコアを計算する
                    img_face = img[fp[1]:fp[1]+fp[3], fp[0]:fp[0]+fp[2]]
                    img_face = cv2.resize(img_face, (64, 64))
                    score = model.predict(np.expand_dims(img_face, axis=0))
                    # 最も高いスコアを書き込む
                    score_argmax = np.argmax(np.array(score[0]))
                    text =  "{0} {1:.3}% ".format(Labels[score_argmax], score[0][score_argmax]*100)
                    cv2.putText(img, text, (fp[0],fp[1]+fp[3]+30), cv2.FONT_HERSHEY_DUPLEX, 1, (0,0,255), 2)
                # 画面に表示
                cv2.imshow("{}_{}".format(location[0], count), img)
                plt.show()
                image_save(img,"result")
            # 顔が検出されなかったとき
            else:
                continue

7.まとめ

「TinderAPIで女の子の顔写真を集めて、加工アリorナシを自動で判定してみた」はいかがだったでしょうか?

私は、TinderのAPIで取得できる情報は写真以外にもいろいろあるみたいなので、ソーシャル系のデータ解析としていろいろ取り組めることがあるかもしれないなあと感じています。

またプログラミングの課題で扱うような整理されたデータでは考慮しないようなところをいろいろ考えなくてはならないことを身をもって体感することができたことはとてもよい経験でした。正直ディープラーニングの学習モデルの出来としてはまだまだだと思いますが、モデルの構築理論などをしっかり勉強してより良いモデルが作れるように精進していきたいと思います。

最後までお付き合いくださり、ありがとうございました!