この記事で紹介すること
- Deep Learning(RBM)を使って遊んでみる1事例としてミスコン支援アプリを作る
- PyLearn2で簡単に学習を実行
- Pythonコードでインタラクティブな散布図を作成する
はじめに
Deep Learningを使ったタスクの応用事例として、「ミスコン支援アプリを作る」ということをやってみました。
いまは便利な世の中で、大学ミスコンのポータルサイトが存在します。
出場者が多く、楽しみがいもあります。
しかし、同時に、全員をまとめて眺めてみたい気もしました。
そして、似た顔をまとめてみたいとも思いました。(邪道なのかもしれませんが、少なくとも私はそうやってミスコンを楽しんでいます)
そこで、今回は次の目標を設定します
- インタラクティブな散布図を作る。
- 散布図をみて、似た顔が集まっていれば成功
- ミスコン支援したいので、投票するモチベーションが高くなればOK(個人的にモチベーションがわけば、それで十分です)
そこで、次の作業フローを考えました。
- 出場者の情報を獲得する
- 顔画像部分だけ切り出し
- 類似した顔を捉えられる特徴量を学習する
- 学習した特徴量ベクトルをつかって、画像データを空間写像する(Embeddingとも呼ばれます)
- 次元空間まで圧縮をする
- インタラクティブな散布図をつくる
この作業をすべてPythonでこなすことにします。
それぞれ、Pythonのライブラリを使えば実現可能です。
1.の部分は普通にスクレイピングコードを書くだけなので割愛します。
なお、記事の多くで機械学習系のライブラリを使います。
機械学習系のライブラリに強い、__Anaconda__ディストリビューションに切り替えておきましょう。参考記事
また、MacOSX 10.10/Python2.7をつかっている環境で話を進めます。
2. 顔画像部分だけを切り出し
獲得できる写真はいずれも顔だけでなく、全身と背景の風景が写っています。
例えば、このような感じです
今回は、__似た顔を寄せる__ことが目的なので、全身と背景は不要な情報です。
そこで、自動的に顔部分だけを切り出すことにします。
この操作はOpenCV+cv2の組み合わせで実現できます。
インストール
anacondaのパッケージ管理ツールのconda
でopencvがインストール可能です。
$ conda install -c https://conda.binstar.org/menpo opencv
インストールの確認をしてみましょう
>>> import cv2
>>> cv2.__version__
'2.4.9.1'
>>> exit()
使い方
まずは、画像を読み込みます
import cv2
image = cv2.imread(path_to_imagefile)
顔領域特定
つぎに顔がある領域を判断するための設定をします。
顔領域を判定するためには、機械学習モデルが必要です。openCVにはすでに__カスケード特徴量__と呼ばれる特徴量で訓練された顔領域判定モデルが用意されています。
顔領域判定モデルはxmlファイルで保存されているので、パスを指定します。
見つからない方はfind
コマンドなんかでhaarcascade_frontalface_alt.xml
を探してみてください。
パスを定数に指定しておきます。
CASCADE_PATH = "/Users/kensuke-mi/.pyenv/versions/anaconda-2.1.0/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml"
顔領域を特定する前に、グレースケール化しておきます。
(顔領域の特定にはグレースケール化は必要ありません。Deep NNで学習するための前処理です。)
image_gray = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)
さらに画像行列の数値を均一化しておきます。詳しくはマニュアルをみましょう。
image_gray = cv2.equalizeHist(image_gray)
最後に、顔領域判定器を呼び出して、領域を求めます
cascade = cv2.CascadeClassifier(CASCADE_PATH)
facerect = cascade.detectMultiScale(image_gray, scaleFactor=1.1, minNeighbors=3, minSize=(50, 50))
facerect
には顔画像の領域座標が返されます。
一連の流れを関数化してみました。
def detectFace(image):
image_gray = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)
image_gray = cv2.equalizeHist(image_gray)
cascade = cv2.CascadeClassifier(CASCADE_PATH)
facerect = cascade.detectMultiScale(image_gray, scaleFactor=1.1, minNeighbors=3, minSize=(50, 50))
return facerect
画像切り出し
すでに領域はわかっているので、imaga
オブジェクトから領域を切り出すだけで顔部分が抽出できます。
これも関数化しました。
def extract_face(facerect_list, image, path_to_save):
"""顔の部分を切り出す。ただし、顔は1つの写真に一人しかいない前提なので注意
:param facerect_list:
:param image:
:param path_to_save:
:return:
"""
assert os.path.exists(os.path.dirname(path_to_save))
for rect in facerect_list:
x = rect[0]
y = rect[1]
w = rect[2]
h = rect[3]
# img[y: y + h, x: x + w]
cv2.imwrite(path_to_save, image[y:y+h, x:x+w])
return image[y:y+h, x:x+w]
画像リサイズ
画像のサイズを均一化します。
DeepNNで効率よく学習するためです。
サイズが異なっていても、学習できるにはできるのですが、計算量が多くなってしまうので、ここでサイズを揃えます。
image
オブジェクトをim
として、リサイズ後のサイズを指定して、リサイズを実行します。
RESIZED_TUPLE = (100, 100)
resized_im = cv2.resize(im,size_tuple)
一連の流れはgithubにありますので、参考にしてください。
3 類似した顔を捉えられる特徴量を学習する
Deep NN(Neural Network)の一種であるRBMを利用します。
RBMの動作原理はこの記事が優れていますので、参考にするとよいです。
RBMを利用するため、Pylearn2
ライブラリを利用します。
Deep Learningというと、PFIのchainer
が有名ですが、chainer
はRBMを利用するための学習法をサポートしていません(2015/10/25現在)
RBMでなく、Auto Encoder系のネットワークを利用されるのなら、chainer
をお勧めします。
インストール
基本的には、Gitリポジトリをクローンして、インストールをするだけです。
git clone git://github.com/lisa-lab/pylearn2.git
cd pylearn2
python setup.py build
sudo python setup.py install
この後、「pylearn2が利用するパスを通す」みたいな解説をよく見ますが、実はパスを通さなくともPylearn2は動きます。
ただ、チュートリアルコードを動かすためにはやはりパスを通さなくてはいけません。
git cloneからチュートリアルコードの実行までをシェルスクリプトにしたので、参考にしてみてください。
(動かなかったら、ごめんなさい)
使ってみる
pylearn2の大まかな流れはこんな感じです
- 訓練用のデータをpylearn2で使える形式にする
- 訓練方法をyamlファイルに書く
- 訓練スクリプトを実行する
1 訓練用のデータをpylearn2で使える形式にする
学習データはnumpyのndarrayで用意をして、pylearn2用のオブジェクトに変換します。
ここでは、すでにnumpy.ndarray
でデータが用意されているものとします。
まずは、pylearn2用に形式に書き換えるクラスを用意します。
ここでは、「顔画像のデータ」という意味で、FacePicDataSet
というクラスを用意します。このクラスはpylearn2.datasets.DenseDesignMatrix
を継承しています。
from pylearn2.datasets import DenseDesignMatrix
class FacePicDataSet(DenseDesignMatrix):
def __init__(self, data):
self.data = data
super(FacePicDataSet, self).__init__(X=data))
つぎにFacePicDataSet
オブジェクトを作ります
train = FacePicDataSet(data=data_matrix)
つぎに、nupmy形式のファイルとデータセットpickleファイルを保存します。
from pylearn2.utils import serial
train.use_design_loc(path_to_npy_file)
# save in pickle
serial.save(train_pkl_path, train)
2. 訓練方法をyamlファイルに書く
基本的にはチュートリアルのテンプレートを持ってきて、いじる感じです。
訓練用のスクリプトから呼び出して変数を突っ込むところは%(変数名)型
で記述します。
ポイントは
-
raw: &raw_train !pkl:
にpickleファイルのパスを記述 -
nvis:
にデータの次元数を記述(100*100画像の場合、10000次元です) -
save_path:
に訓練後のデータ保存先を記述
長くなるので全文は私のyamlファイルを参考にしてください。
3. 訓練スクリプトを実行する
まずは利用パッケージをimportします。
データセットpickleファイルのクラスを__必ず忘れずにimportしてください__
import os
from pylearn2.testing import skip
from pylearn2.testing import no_debug_mode
from pylearn2.config import yaml_parse
from make_dataset_pylearn2 import FacePicDataSet
まずはtraining用の関数を書きます
@no_debug_mode
def train_yaml(yaml_file):
train = yaml_parse.load(yaml_file)
train.main_loop()
さらに、yamlファイルを読んで、変数の穴埋めをします。
yaml_file_path = path_to_yaml_file
save_path = path_to_save_file
yaml = open(yaml_file_path, 'r').read()
hyper_params = {'detector_layer_dim': 500,
'monitoring_batches': 10,
'train_stop': 50000,
'max_epochs': 300,
'save_path': save_path,
'input_pickle_path': input_pickle_path}
yaml = yaml % (hyper_params)
最後に、trainingを実行します
train_yaml(yaml)
番外編:可視化する
DeepNNは隠れ層を持っていて、この隠れ層が抽出された特徴量にあたります。
せっかくなので、隠れ層を抽出するスクリプトも作成しました。
要はnupmy.ndarray
の数値を使って、画像を作成するだけです。
細かい説明は省くので、こちらをご覧ください。
実行すると、このように隠れ層のノード数分だけの画像が表示されます。
4. 学習した特徴量ベクトルをつかって、画像データを空間写像する
学習された特徴量を使って、元のデータを写像します。
例えば、今回は、元のデータには写真が711枚あり、それぞれの画像は150*150=22,500次元です。
ですので、元データの行列は711*22500
で構成されています。
一方、特徴量変換行列は(元の次元数)*(隠れ層のノード数)
です。
今回は、500個の隠れ層ノードを用意しているので、22500*500
の行列です。
なので、変換後は (711*22500) * (22500*500) = 711*500
の行列になります。
元データ読み込み
711*22500
の行列データを読み込みます。
訓練データpickleファイルを読み込めばいいらしいのですが、うまくいきませんでした。FacePicDataSet
クラスが原因で何かしろのエラーが発生してしまいます。
なので、今回は、numpyファイルを読み込むこととします。
import numpy
datasource_ndarray = numpy.load(path_to_datasource_npy)
で、元のデータが読み込みされました。
つぎに訓練済みのpickleオブジェクトファイルを読み込みます。
import pickle
file_obj = open(path_to_trained_model_pickle, 'rb')
model_object = pickle.load(file_obj)
さらに、隠れ層の重み行列を取得します
feature_vector = model_object.get_weights()
最後に、空間写像します
feature_vector
は転地されているので、T
メドッドで転地します。
new_space_matrix = numpy.dot(a=data_matrix, b=feature_vectors.T)
これで変換済みの行列new_space_matrix
が得られます。
この操作は一般にembedding
と呼ばれます。
5. 次元空間まで圧縮をする
Embeddingしたデータを圧縮します。
というのも、今回は2次元の散布図にしたいので、500次元の空間でもまだ大きいからです。
次元圧縮には、tSNE・PCAがよく使われることが多いです。
(と思う経験的に)
この2つはscikit-learnを使えば簡単に実行できます。tSNE, pca
詳しい説明は割愛しますので、scikit-learnのexampleや私のコードをご覧ください。
6. インタラクティブな散布図をつくる
インタラクティブな散布図を作成します。
せっかくなので、いくつか機能が欲しいところです。ミスコン支援なので
- 顔画像が表示されるようにしたい
- プロフィールもみたい
- プログページにジャンプできるようにしたい
の機能は欲しいところです。
それ、ぜんぶできます。Bokehならね!
インストール
anacondaで利用しているのなら、とても簡単です。
Bokehの公式ページに書いてあるコマンドですぐ終わります。
ただし、標準のipython notebookは置き換えられてしまうので、注意しましょう。
いまさらながら、pyenvでglobalのpython環境を切り替えられるようにしておくといいです。
グラフ作成
Bokehのナイスな点はipython notebookで作成できる点です。
私は、ipython notebookを使って作成しました。
なんだか、説明が前後してしまいますが、いまはこんな感じでdictオブジェクトを持っているとします。
出場者の人数分だけ、キーと値が保存されています。
{
string: {
"major": string,
"grade": int,
"age": int,
"member_name_rubi": string,
"height": float,
"member_index": int,
"profile_url": string,
"blog_url": string,
"member_name": string,
"university": string,
"position_vector": [
float,
float
],
"photo_url": string
}
Bokehでグラフを作成するには、大まかにはこんな感じで操作します
- Bokehテーブルオブジェクト作成
- テーブルオブジェクトからグラフオブジェクト作成
-
show(グラフオブジェクト)
でhtml埋め込みグラフ作成
1. Bokehテーブルオブジェクト作成
Bokehのテーブルオブジェクトを作成するには、listデータを複数用意して、ColumnDataSource
メソッドを利用します。
さきほどのdictオブジェクトを変形して、こんな感じのデータ構造にします
{
'X': [ここら辺にデータ],
'Y': [ここら辺にデータ],
'desc': [ここら辺にデータ],
'imgs': [ここら辺にデータ],
キーとvalueに組み合わせ
}
XとかYとはdescとかいうキーの名前はなんでもいいです。
当たり前ですが、valueのlistはすべて同じ長さでなければいけません。
items_for_graphというオブジェクト名で保存しておきます。
まずは、Bokeh用のテーブルオブジェクトを作成します。
from bokeh.plotting import figure, output_file, show, ColumnDataSource
source = ColumnDataSource(
data=dict(
x=items_for_graph['X'],
y=items_for_graph['Y'],
desc=items_for_graph['labels'],
imgs = items_for_graph['images'],
univ = items_for_graph['universities'],
major = items_for_graph['major'],
height = items_for_graph['height'],
age = items_for_graph['age'],
blog = items_for_graph['blog_links'],
profile= items_for_graph['profile_links'],
)
)
つぎに、グラフで使いたい機能を指定したり、グラフサイズを指定したりします。
Toolsの中には、グラフで利用したい機能を指定します。
詳しくはBokehのマニュアルをみましょう。
from bokeh.io import output_file, show, vform, vplot
from bokeh.models import HoverTool, OpenURL, TapTool
# Import bokeh, sub modules for making scallter graph with tooltip
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.models import ColumnDataSource, OpenURL, TapTool
from bokeh.plotting import figure, output_file, show
GRAPH_HEIGHT = 1000
GRAPH_WIDTH = 800
TOOLS = [WheelZoomTool(), PanTool(), ResetTool(), TapTool()]
そして、グラフオブジェクトを作成します。
今回は、circleメソッドを指定して、散布図にしています。ここを変えると、線を引いて時系列を表示できたりします。
このあたりもBokehのマニュアルをみましょう。
s1 = figure(plot_width=GRAPH_WIDTH, plot_height=GRAPH_HEIGHT, tools=TOOLS, title=graph_title_name)
s1.circle('x', 'y', size=10, source=source)
最後にshow(s1)
でグラフhtmlを作成します。
いろいろ機能つける
顔画像表示
Bokehでは実はhtmlタグを自分で書いて、ツールチップをつくれます。
なので、画像を表示するhtmlタグを書いて、ツールチップにすれば良いわけです。
詳しくはこちら
クリックしたらURL開く
OpenURL
メソッドを利用すれば、簡単にURLを埋め込むことができます。
クリックすると、テーブルにあるURLを開きます。
詳しくはこちらを
テーブルとグラフを両方とも表示
グラフの中にすべての情報を表示することはできません。
そこで、細かい情報はテーブルに入れてしまうことにします。
そして、上には散布図、下にはグラフ。という様に表示すればよいわけです。
詳しくはこの部分をみましょう。
そんなわけで、私が作成したグラフはこちら。このグラフはRBM embeddings+PCAで2次元にした結果です。
tSNEの方も見てみましょう。
比較してみると、PCAの方がしっかりと似た顔が近くにプロットされている気がします。
PCAの方がうまくEmbeddingsの次元を2次元まで圧縮できているような気がします。
そんなわけで、今回は、Deep Learningを使って、ミスコンを支援する何かを作ってみました。
ぜひ、Deep Learningの1活用事例と思っていただけたら幸いです。
あと、すべてのコードはgithubで公開しています。ぜひ、ご活用ください。あと、一緒におもしろ開発をしてくださる方を募集中でございます。おもしろ活用のアイディアがありましたら、ぜひご連絡ください。