PyLearn2を使ってミスコン支援アプリを作る

# この記事で紹介すること

* Deep Learning(RBM)を使って遊んでみる１事例としてミスコン支援アプリを作る
* PyLearn2で簡単に学習を実行
* Pythonコードでインタラクティブな散布図を作成する


# はじめに

Deep Learningを使ったタスクの応用事例として、「ミスコン支援アプリを作る」ということをやってみました。

いまは便利な世の中で、[大学ミスコンのポータルサイト](https://misscolle.com/)が存在します。

出場者が多く、楽しみがいもあります。
しかし、同時に、全員をまとめて眺めてみたい気もしました。
そして、似た顔をまとめてみたいとも思いました。（邪道なのかもしれませんが、少なくとも私はそうやってミスコンを楽しんでいます）

そこで、今回は次の目標を設定します

* インタラクティブな散布図を作る。
* 散布図をみて、似た顔が集まっていれば成功
* ミスコン支援したいので、投票するモチベーションが高くなればOK（個人的にモチベーションがわけば、それで十分です）


そこで、次の作業フローを考えました。

1. 出場者の情報を獲得する
2. 顔画像部分だけ切り出し
3. 類似した顔を捉えられる特徴量を学習する
4. 学習した特徴量ベクトルをつかって、画像データを空間写像する（Embeddingとも呼ばれます）
5. 次元空間まで圧縮をする
6. インタラクティブな散布図をつくる

この作業をすべてPythonでこなすことにします。

それぞれ、Pythonのライブラリを使えば実現可能です。

1.の部分は普通にスクレイピングコードを書くだけなので割愛します。

なお、記事の多くで機械学習系のライブラリを使います。

機械学習系のライブラリに強い、__Anaconda__ディストリビューションに切り替えておきましょう。[参考記事](http://qiita.com/ken0nek/items/c51b8e2b83d902698fdf)

また、MacOSX 10.10/Python2.7をつかっている環境で話を進めます。

# 2. 顔画像部分だけを切り出し

獲得できる写真はいずれも顔だけでなく、全身と背景の風景が写っています。
例えば、[このような感じです](https://misscolle.com/sophia2015/photo#1)

今回は、__似た顔を寄せる__ことが目的なので、全身と背景は不要な情報です。

そこで、自動的に顔部分だけを切り出すことにします。

この操作は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)
```

さらに画像行列の数値を均一化しておきます。詳しくは[マニュアル](http://opencv.jp/opencv-2svn/cpp/histograms.html#cv-equalizehist)をみましょう。

```
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):
    """顔の部分を切り出す。ただし、顔は１つの写真に一人しかいない前提なので注意
    :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](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls/tree/master/data_process)にありますので、参考にしてください。


# 3 類似した顔を捉えられる特徴量を学習する

Deep NN(Neural Network)の一種であるRBMを利用します。
RBMの動作原理は[この記事](http://qiita.com/t_Signull/items/f776aecb4909b7c5c116)が優れていますので、参考にするとよいです。

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からチュートリアルコードの実行までを[シェルスクリプトにした](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls/blob/master/analysis/using_pylearn2/setup_procedure.sh)ので、参考にしてみてください。
（動かなかったら、ごめんなさい）


### 使ってみる

pylearn2の大まかな流れはこんな感じです

1. 訓練用のデータをpylearn2で使える形式にする
2. 訓練方法をyamlファイルに書く
3. 訓練スクリプトを実行する


### 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ファイル](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls/blob/master/analysis/deep_learning/core_pylearn2/model_pylearn2/rbm.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`の数値を使って、画像を作成するだけです。

細かい説明は省くので、[こちら](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls/blob/master/analysis/deep_learning/core_pylearn2/show_weight_pylearn2.py)をご覧ください。

実行すると、このように隠れ層のノード数分だけの画像が表示されます。

![スクリーンショット 2015-10-25 20.47.30.png](https://qiita-image-store.s3.amazonaws.com/0/33533/33fd41a5-c906-6116-fa2a-1b0b908cea38.png)

# 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したデータを圧縮します。

というのも、今回は２次元の散布図にしたいので、500次元の空間でもまだ大きいからです。

次元圧縮には、tSNE・PCAがよく使われることが多いです。
（と思う経験的に）

この２つはscikit-learnを使えば簡単に実行できます。[tSNE](http://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html), [pca](http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html)

詳しい説明は割愛しますので、scikit-learnのexampleや[私のコード](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls/blob/master/analysis/dimension_reduction/modules/reduction_core.py)をご覧ください。


# 6. インタラクティブな散布図をつくる

インタラクティブな散布図を作成します。

せっかくなので、いくつか機能が欲しいところです。ミスコン支援なので

* 顔画像が表示されるようにしたい
* プロフィールもみたい
* プログページにジャンプできるようにしたい

の機能は欲しいところです。

それ、ぜんぶできます。Bokehならね！

### インストール

anacondaで利用しているのなら、とても簡単です。

[Bokehの公式ページ](http://bokeh.pydata.org/en/0.10.0/docs/installation.html)に書いてあるコマンドですぐ終わります。

ただし、標準の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でグラフを作成するには、大まかにはこんな感じで操作します

1. Bokehテーブルオブジェクト作成
2. テーブルオブジェクトからグラフオブジェクト作成
3. `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のマニュアル](http://bokeh.pydata.org/en/0.10.0/docs/user_guide/tools.html)をみましょう。

```
    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のマニュアル](http://bokeh.pydata.org/en/0.10.0/docs/reference/plotting.html)をみましょう。

```
    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タグを書いて、ツールチップにすれば良いわけです。

詳しくは[こちら](http://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#custom-tooltip)

#### クリックしたらURL開く

`OpenURL`メソッドを利用すれば、簡単にURLを埋め込むことができます。

クリックすると、テーブルにあるURLを開きます。

詳しくは[こちらを](http://bokeh.pydata.org/en/latest/docs/user_guide/interaction.html#openurl)

#### テーブルとグラフを両方とも表示

グラフの中にすべての情報を表示することはできません。
そこで、細かい情報はテーブルに入れてしまうことにします。

そして、上には散布図、下にはグラフ。という様に表示すればよいわけです。

詳しくは[この部分](http://bokeh.pydata.org/en/dev/docs/user_guide/layout.html#vertical-layout)をみましょう。

- - -

そんなわけで、私が作成したグラフは[こちら](http://kensuke-mi.pussycat.jp/univ_contest_project_pics/pages/girls_pics_deepNN_pca_scatter.html)。このグラフはRBM embeddings+PCAで２次元にした結果です。

[tSNEの方](http://kensuke-mi.pussycat.jp/univ_contest_project_pics/pages/girls_pics_deepNN_scatter.html)も見てみましょう。

比較してみると、PCAの方がしっかりと似た顔が近くにプロットされている気がします。

PCAの方がうまくEmbeddingsの次元を２次元まで圧縮できているような気がします。

- - -

そんなわけで、今回は、Deep Learningを使って、ミスコンを支援する何かを作ってみました。
ぜひ、Deep Learningの１活用事例と思っていただけたら幸いです。

あと、すべてのコードは[githubで公開](https://github.com/Kensuke-Mitsuzawa/let_s_analyze_miss_collection_girls)しています。ぜひ、ご活用ください。あと、一緒におもしろ開発をしてくださる方を募集中でございます。おもしろ活用のアイディアがありましたら、ぜひご連絡ください。

# その他 - 参考事例

* [pylearn2を簡単に使えるパイプラインを作られた方](http://cordea.hatenadiary.com/entry/2015/02/22/165713)
* [データをcsvにして、twitterアイコンの学習を試された方](http://laughing.hatenablog.com/entry/2013/11/30/pylearn2_%E5%85%A5%E9%96%80%E3%81%97%E3%81%9F%E3%81%84%E7%B7%A8)
* [pylearn2のインストールとチュートリアル](http://qiita.com/mizuki0420@github/items/3f9c97b32e8d6e37abad)
* [pylearn2で手書き文字認識](http://qiita.com/dsanno/items/a1d805a89e192c44730d)
* [人工データを使ってpylearn2を使って見る](http://qiita.com/shima_x/items/e3579861329fa8d1cd8e)