Asial Blog

Recruit! Asialで一緒に働きませんか?

機械学習で女性のタイプ判定処理を行う

カテゴリ :
バックエンド(プログラミング)
タグ :
機械学習
Python
SVM
OpenCV
画像解析
こんにちは、江口です。

前回に引き続き、pythonネタです。

皆さんは、同僚との会話で「この女優、どう?」と聞かれて困ったことはありませんか?
私はあります。そして、良いとも悪いとも思えず、なんと言いますか、興味が湧かない時が一番返答に困ります。

今回はズバリ「機械学習で好みの女性なのかどうか仕分ける!」です。
ネタではありますが、真剣です。これによりデータに基づいた返答ができるようになります。

▼概要
1. 自分好みの女優の画像とそうでない画像を収集する。
2. 収集した画像から顔だけを抽出し、100x100にリサイズして保存する。
3. SVM分類器にかけて、機械学習を行い、学習結果をpickleに漬け込む
4. 女優画像が自分の好みかどうか判定する

こんな流れになってます。
詳しい流れ、実装は後述するソース解説で紹介します。

▼使用技術とその用途
・python 3.6
・scikit-learn -> 機械学習ライブラリ
・GoogleCustomSearch API -> 画像取得に使用
・BeautifulSoup -> スクレイピングに使用
・SVM分類器 -> 実際に機械学習するアルゴリズム
・OpenCV -> 画像から顔・輪郭抽出するのに使用
・joblib->pickleファイル漬け込み

▼1. 自分好みの女優の画像とそうでない画像の収集


収集はsave_beautiful.pyで行います。
  1. # -*- coding: utf-8 -*-
  2. import requests
  3. import re
  4. import os
  5. from PIL import Image as resizer
  6. API_URL = "https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=%s&searchType=image&start=%s"
  7. def getImageFromCustomAPI(start, end, word, kind):
  8.     for i in range(start, end, 10):
  9.         print(API_URL % (word, str(i)))
  10.         response = requests.get(API_URL % (word + ",正面", str(i)))
  11.         for j in range(len(response.json()["items"])):
  12.             json = response.json()["items"][j]
  13.             res = requests.get(json["link"], stream=True)
  14.             if res.status_code == 200:
  15.                 with open("images/target_%s_%s.png" % (kind, (str(i) + "_" + str(j))), 'wb') as file:
  16.                     for chunk in res.iter_content(chunk_size=1024):
  17.                         file.write(chunk)
  18. def resizeImages(dirname, regular):
  19.     """
  20.     images下の画像を2次元の特徴ベクトルに変換する為に100x100リサイズを行う
  21.     """
  22.     for image in os.listdir(dirname):
  23.         if re.match(regular, image):
  24.             print(image)
  25.             img = resizer.open("images/" + image, 'r')
  26.             img = img.resize((100, 100))
  27.             img.save("images/" + image, 'png', quality=100, optimize=True)
  28.     print("resized")
  29. def main():
  30.     # 「人名,正面」にすると、より多くの正面画像を拾える
  31.     words = ['北川景子', '吉高由里子', '新垣結衣', '榮倉奈々', '安室奈美恵', '長澤まさみ', '西内まりや', '麻生久美子', '倉科カナ', '井上真央', '石原さとみ', 'ガッキー', '堀北真希']
  32.     # words = ['フィーフィー', '安藤なつ', '澤穂希', '白鳥久美子', '光浦靖子', 'ブルゾンちえみ', 'おかずクラブオカリナ']
  33.     for word in words:
  34.         getImageFromCustomAPI(1, 92, word, "YES-" + word + "_")
  35.     resizeImages("images", "target_")
  36. if __name__ == '__main__':
  37.     main()

解説・・・

画像の収集にはGoogleCustomSearchを利用しています。
qパラメータに自動で「,正面」という文字列を組み立て、searchインデックス(startパラメータ)に1~91までを10区切りで指定しています。
つまり、
  1. https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=[新垣結衣]&searchType=image&start=1
  2. https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=[新垣結衣]&searchType=image&start=11
  3. https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=[新垣結衣]&searchType=image&start=21
  4. https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=[新垣結衣]&searchType=image&start=31
  5. https://www.googleapis.com/customsearch/v1?key=[APIKEY]&cx=[ENGINE]&q=[新垣結衣]&searchType=image&start=91

このような具合で順繰りにリクエストを発行していきます。
一回にリクエストで10枚、一人の女優につき1~91でつまり100枚近くの画像を収集しています。
今回は無料枠で利用しているため、以下のような制約があります。

・100req/day(17:00にリセットされる)
・一回に取得できるデータ量は10count

取得した画像は残さずimages/下に

  1. target_YES-[女優名]__81_1.png
の形で保存しています。YESがタイプで、NOがタイプでないことを表すようにしています。
また、なるべく正面を向いている画像を取得しようと試みています。

最後に100x100にリサイズして改めて保存しています。

こんな感じで、私のタイプの女優画像が蓄積されていきます!



▼2.収集した画像から顔だけを抽出して保存



好みの女優の系統を判定したいので、背景や服のような副次的情報は不要ですし、むしろ判断を難しくさせますので、純粋に顔だけを抽出しにいきます!

・pickup-faces.py

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import cv2
  4. def main():
  5.     """
  6.     images下の美人画像一覧を読み出し、faces下に顔画像検出した結果を保存する
  7.     """
  8.     for image_path, _, files in os.walk('images'):
  9.         if len(_):
  10.             continue
  11.         face_path = image_path.replace('images', 'faces')
  12.         if not os.path.exists(face_path): os.makedirs(face_path)
  13.         for filename in files:
  14.             if not filename.startswith('.'):
  15.                 save_faces(image_path, face_path, filename)
  16. def save_faces(image_path, face_path, filename):
  17.     """
  18.     真正面顔判定用のOpenCVファイルを使って、顔画像を切り出す
  19.     """
  20.     print(image_path, face_path, filename)
  21.     # カスケード分類器を読み込む(正面顔の検出分類器)
  22.     cascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_alt2.xml')
  23.     image = cv2.imread('{}/{}'.format(image_path, filename))
  24.     gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  25.     faces = cascade.detectMultiScale(gray_image)
  26.     # Extract when just one face is detected
  27.     if (len(faces) == 1):
  28.         (x, y, w, h) = faces[0]
  29.         image = image[y:y+h, x:x+w]
  30.         image = cv2.resize(image, (100, 100))
  31.         cv2.imwrite('{}/{}'.format(face_path, filename), image)
  32.     else:
  33.         cv2.imwrite('{}/{}'.format(face_path + "/misses", filename), image)
  34.         print("skipped.")
  35. if __name__ == '__main__':
  36.     main()

解説・・・

顔・輪郭の画像抽出にはOpenCVを利用しています。
imagesフォルダにあるYES or NO画像を全てOpenCVの顔抽出分類器にかけて、うまいこと輪郭画像が抽出できたら、faces下に顔画像を保存しています。

参考になるように、失敗した場合は、faces/misses下に保存しています。
missesを見ると大体が横顔画像だったり、複数人写っていたり、遠すぎたり、影りが多かったりとする画像が殆どでした。

faces下の画像はこんな感じです。




▼3. SVM分類器にかけて、機械学習を行い、学習結果をpickleに漬け込む



save_vector3.py

  1. # -*- coding: utf-8 -*-
  2. """
  3. Face画像特徴ベクトルに変換し、保存する
  4. """
  5. from PIL import Image
  6. import os
  7. import re
  8. import numpy as np
  9. from sklearn.externals import joblib
  10. from sklearn import svm
  11. def convertImageVector3(img):
  12.     """
  13.     3次元(100x100x3(RGB))から1次元に変換する
  14.     """
  15.     s = img.shape[0] * img.shape[1] * img.shape[2]
  16.     img_vector3 = img.reshape(1, s)
  17.     return img_vector3[0]
  18. def getDatas():
  19.     """
  20.     3次元ベクトル学習画像データと正解ラベルを対にして、pickleファイルにして保存する
  21.     """
  22.     files = ["faces/" + f for f in os.listdir("faces") if re.match("target_", f)]
  23.     labels = []
  24.     datas = []
  25.     for image in files:
  26.         as_arrayed_img = np.asarray(Image.open(image))
  27.         # 3次元かどうか
  28.         if (len(as_arrayed_img.shape) == 3):
  29.             # RGBが3がどうか
  30.             if (as_arrayed_img.shape[2] == 3):
  31.                 datas.append(convertImageVector3(np.asarray(Image.open(image))))
  32.                 # 「YES」と「NO」を抽出
  33.                 labels.append(image.split("faces/target_")[1].split("-")[0].replace("reversed_", ""))
  34.             else:
  35.                 print("skip not rgb3")
  36.     print("converted.")
  37.     return (datas, labels)
  38. def learn(datas, labels):
  39.     """
  40.     データを学習
  41.     """
  42.     clf = svm.LinearSVC()
  43.     clf.fit(datas, labels)
  44.     joblib.dump(clf, "pkls/beauty.pkl")
  45.     print("learned.")
  46. if __name__ == '__main__':
  47.     datas, labels = getDatas()
  48.     print(len(datas), len(labels))
  49.     learn(datas, labels)

解説・・・

かいつまんで言いますと、画像を3次元のベクトルから1次元に変換をかけた形で、正解ラベル(YES or NO)と一緒にパターンを学習させるということを行います。
俗にいう「教師あり学習」を行なっています。

学習にはSVMというアルゴリズムを使用し、YESつまり「私のタイプである女性の画像はこのような3次元ベクトルを有しています。」また、「私のタイプでない女性の画像はこのような3次元ベクトルを有しています。」と識別基準を教え込ませています。

SVMにより、YESとNOの境界値最大マージンを取り、識別を行います。

scikit-learnでSVM使った学習は

  1. clf = svm.LinearSVC()
  2. clf.fit(datas, labels)

このようにdataと正解のlabelをセットすることで学習させることができますが、永続化されません。

そのため、Objectを直列化し、学習データをpickleと呼ばれる形式で漬物保存します。

  1. joblib.dump(clf, "pkls/beauty.pkl")

こうすることで、予測実行時に毎回学習し直すことなく、pklファイルをロードすることで機械学習過程を引き継いで予測・判定を行うことが可能になります。

例えば、学習したその場で予測判定を行うケースは

  1.     clf = svm.LinearSVC()
  2.     clf.fit(datas, labels)
  3.  res = clf.predict(t_image_vector3)

このような記載になるかと思われます。
しかし、学習は結構な計算負担があるため、毎回学習計算を走らせず、pklで学習記録を読み出すことができます。

その場合は以下のような記載になります。

  1.     clf = joblib.load(learnedFile)
  2.     # 予測開始
  3.     res = clf.predict(t_image_vector3)


▼4. 女優画像が自分の好みかどうか判定する



いよいよ判定です。
今のところ、imagesに1980の画像と、facesにその半分の1100画像が格納されています。
(顔輪郭抽出時に多くが失敗し、データが半減しました。。)

これらを学習した結果、「ガッキー」は私のタイプなのかを判定します!

judge.py

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import io
  4. import urllib.request
  5. from PIL import Image
  6. from sklearn.externals import joblib
  7. import numpy as np
  8. import cv2
  9. from datetime import datetime as dt
  10. import requests
  11. from bs4 import BeautifulSoup as bs
  12. import uuid
  13. def convertImageVector3(img):
  14.     """
  15.     3次元(100x100x3(RGB))から1次元に変換する
  16.     """
  17.     s = img.shape[0] * img.shape[1] * img.shape[2]
  18.     img_vector3 = img.reshape(1, s)
  19.     return img_vector3[0]
  20. def saveFaceImg(img):
  21.     image = np.asarray(img)
  22.     cascade = cv2.CascadeClassifier('haarcascades/haarcascade_frontalface_alt2.xml')
  23.     faces = cascade.detectMultiScale(image)
  24.     if (len(faces) == 1):
  25.         (x, y, w, h) = faces[0]
  26.         image = image[y:y+h, x:x+w]
  27.         image = cv2.cvtColor(cv2.resize(image, (100, 100)), cv2.COLOR_BGR2RGB)
  28.         cv2.imwrite('{}/{}'.format("targets", dt.now().strftime("%Y%m%d%H%M%S") + "_" + str(uuid.uuid1()) + ".png"), image)
  29.         return image
  30.     return ""
  31. def main(imgs):
  32.     # 試験データをhttp経由で取得し、100x100にリサイズ
  33.     t_image_vector3 = []
  34.     for img in imgs:
  35.         target = saveFaceImg(Image.open(io.BytesIO(urllib.request.urlopen(img).read())).resize((100, 100)))
  36.         if (len(target)):
  37.             print(img)
  38.             t_image_vector3.append(convertImageVector3(target))
  39.     # 学習済みデータの取得
  40.     learnedFile = os.path.dirname(__file__) + "/pkls/beauty.pkl"
  41.     clf = joblib.load(learnedFile)
  42.     # 予測開始
  43.     res = clf.predict(t_image_vector3)
  44.     for ret in res: print(ret)
  45. if __name__ == '__main__':
  46.     SEARCH_URL = r"https://www.google.co.jp/search?q=%s&source=lnms&tbm=isch&sa=X&biw=1439&bih=780"
  47.     TARGET_IMG_SRC_PATTERN = r"https://encrypted-"
  48.     targets = []
  49.     html = requests.get(SEARCH_URL % "ガッキー").text
  50.     # BeautifulSoupでHTMLを解析
  51.     soup = bs(html, 'html.parser')
  52.     target_src = []
  53.     images = soup.find_all('img')
  54.     for img in images:
  55.         targets.append(img['src'])
  56.     main(targets)
  57.     # main(['http://blog-imgs-42.fc2.com/m/e/m/memoriup/03267.jpg'])

解説・・・

Google画像検索をスクレイピングしています。
画像検索で「ガッキー」で検索させ、imgタグのsrc属性を抽出して、その画像URLの取得画像結果を分類器にかけています!
URLから画像を取得し、取得した画像をそのままresizeにかける記載は以下の通りです。

  1. Image.open(io.BytesIO(urllib.request.urlopen("http://画像のURL").read())).resize((100, 100))

ソースでは、このresizeした画像をcv2モジュールで顔画像抽出した画像をtargets下に保存し、いよいよ判定コードにかけています。

それがこちらです。

  1.     # 学習済みデータの取得
  2.     learnedFile = os.path.dirname(__file__) + "/pkls/beauty.pkl"
  3.     clf = joblib.load(learnedFile)
  4.     # 予測開始
  5.     res = clf.predict(t_image_vector3)
  6.     for ret in res: print(ret)

上記で表示された結果を共有しますと。。。

  1. NO
  2. YES
  3. YES
  4. YES
  5. NO
  6. YES
  7. NO
  8. NO
  9. YES
  10. YES
  11. YES
  12. NO

となりました。
判定を行なった画像URLもログに出しているのですが、確かに微妙にガッキーではない画像も混じっていたため、
7,80%くらいの確度といった具合のように感じられます。

とはいえ、データが全然数が足りていないので、これから地道に学習データを増やしていこうと思います!!

学習データ量が圧倒的に足りないので、足していきつつも、次回はいよいよTensorflowで多重パーセプトロン、ディープラーニングによる分類・判定を行おうと思います!!

今回も拙い文章、ソースでしたが、お付き合いいただきありがとうございました。
ソースコードはGithubで管理(?)してますので、こちらをちょこちょこ修正していく形になるかと思いますが、ご了承ください。

https://github.com/yueguchi/asial-blog/tree/master/beauty-judge

<<追記>>
学習素材がやはり少ないのが悩みだったので、少し工夫しました。

faces下の画像を全てy軸に反転させることで素材を二倍に増やしました。

copy_faces.py

  1. # -*- coding: utf-8 -*-
  2. """
  3. 女優の画像素材を増やすために、すでにあるfaces下の画像を反転させ、
  4. 画像素材として扱わせることで、二倍の素材が手に入る
  5. """
  6. import os
  7. import cv2
  8. def main(targetDir):
  9.     for path, _, files in os.walk(targetDir):
  10.         for file in files: 
  11.             if not file.startswith('.'):
  12.                 print(path + '/' + file)
  13.                 img = cv2.imread((path + '/' + file), cv2.IMREAD_COLOR)
  14.                 reversed_y_img = cv2.flip(img, 1)
  15.                 cv2.imwrite(path + '/' + file.replace('target_', 'target_reversed_'), reversed_y_img)
  16. if __name__ == '__main__':
  17.     main("faces")

実行後、pklを作り直してください。(save_vector3.pyを実行)
これで素材が二倍になります。

実際に反転させた画像はこんな感じです。



麻生久美子がy軸に反転した形で表示されています。

学習素材を増やしたあとに、今度は「ハーマイオニー」でGoogle画像検索の結果を判定器にかけたところ、100%でした。

  1.     html = requests.get(SEARCH_URL % "ハーマイオニー").text

  1. YES
  2. YES
  3. YES
  4. YES
  5. YES
  6. YES
  7. YES
  8. YES

以上です。

アシアルの会社情報

アシアル株式会社はPHP、HTML5、JavaScriptに特化したWebエンジニアリング企業です。ユーザーエクスペリエンス設計から大規模システム構築まで、アシアルメンバーが各々の専門性を通じてインターネットの進化に貢献します。

会社情報詳細

Python x 自然言語処理でアシアルブログの単語ランキングを算出する

カテゴリ :
バックエンド(プログラミング)
タグ :
Python
MeCab
Webスクレイピング
アシアルブログ
アシアルブログ頻出単語ランキング
PythonでWebスクレイピングしたHTML文字列から、形態素解析した一般名詞のみを抽出し、単語の出現度でランク付けを行います。

Vue.js + vuexによるToDoアプリケーションの実装

カテゴリ :
バックエンド(プログラミング)
タグ :
JavaScript
todo_list.png
Vue.jsとvuexを使用して、ToDoアプリケーションを実装します。

Laravel 5.4でWeb APIを作る

カテゴリ :
バックエンド(プログラミング)
タグ :
PHP
items_json.png
LaravelでWeb APIを作る方法を解説します。

Laravel 5.4で Vue.js開発環境を手軽に作る

カテゴリ :
バックエンド(プログラミング)
タグ :
PHP
JavaScript
hello_laravel.png
Laravelのインストール方法と、フロントエンド開発環境のセットアップ、簡単なVueコンポーネントの作り方を解説します。

2月のソーシャルランチ

カテゴリ :
日常
タグ :
日常
ランチ
グリーンサラダ.JPG
2月のソーシャルランチの概要です。

12月&1月のソーシャルランチ

カテゴリ :
日常
タグ :
日常
ランチ
グルメ
attachment00.jpg
12月と1月のソーシャルランチの模様です。

温泉マーク問題

カテゴリ :
デザイン・UI
タグ :
Onsen UI
マーケティング
newonsen.jpg
この記事はOnsen UI Advent Calendar 2016向けの記事として書きました。

アシアルのマーケティング担当の塚田です。

先日報道されたOnsen UIのマーケティング上、非常に重要なニュースについて書きたいと思います。

Onsen UIが生まれたきっかけ

カテゴリ :
フロントエンド(HTML5)
タグ :
社長BLOG
Monaca
JavaScript
Tech
Cordova
images.png
本記事はOnsen UI Advent Calendar 2016のエントリーです。Onsen UIが生まれたきっかけについて、簡単に紹介したいと思います。

11月のソーシャルランチ

カテゴリ :
日常
タグ :
日常
PB150250.JPG
11月のソーシャルランチの模様です。