類似顔検索を半日で実装する

自社ソーシャルサービスのための実験として簡単ななんちゃって顔認識機能のプロトタイプを作りました。
やることはすごく簡単で python のライブラリを呼び出すだけでほぼ終わりますので、この記事に技術的な面白みは一切ありません（笑）。
こんなに簡単にできるとは思ってなかったというか、そういう情報は検索しても見当たらなかったので共有しておきたいと思います。

# 前提知識
なにもいりません。私は python も機械学習も素人です。

# ライブラリ
https://github.com/ageitgey/face_recognition

このライブラリを使います。dlib という顔認識に強いとされるライブラリのラッパーのようなものです。

# 仕組み
上記ライブラリには [face_encodings](https://github.com/ageitgey/face_recognition/blob/master/examples/face_distance.py)というメソッドがあります。
このメソッドに画像ファイルを渡してやると顔の[ランドマーク](https://www.google.co.jp/search?biw=2339&bih=1240&tbm=isch&sa=1&q=face+landmarks&oq=face+landmarks&gs_l=psy-ab.3..0i19k1j0i7i30i19k1l7.10726.11076.0.11164.4.4.0.0.0.0.142.354.1j2.3.0....0...1.1.64.psy-ab..2.2.256...0i7i30k1.L9q0iFem60Q#imgrc=JXb98Axur796VM:)座標からなるベクトルを返してくれます。

例：

```
[-0.14351621270179749, 0.057226795703172684, 0.07066182047128677, -0.13657408952713013, -0.0695628970861435, -0.09160482883453369, -0.011631922796368599, -0.14444005489349365, 0.1372034251689911, -0.12074219435453415, 0.2784915566444397, -0.1435723453760147, -0.301411509513855, -0.006434443406760693, -0.06611043959856033, 0.21539726853370667, -0.1636665165424347, -0.13245481252670288, -0.08063990622758865, 0.04853415489196777, 0.09177280962467194, -0.01833999715745449, 0.00841446127742529, 0.12095664441585541, -0.08568297326564789, -0.37953001260757446, -0.13193728029727936, -0.03719043731689453, -0.0870690569281578, -0.04294124245643616, -0.038571640849113464, 0.05095953121781349, -0.2148473709821701, -0.041665997356176376, 0.014024296775460243, 0.07775825262069702, -0.034873172640800476, -0.15043900907039642, 0.17863482236862183, -0.030670292675495148, -0.2826652228832245, 0.02874363772571087, 0.09433827549219131, 0.20609621703624725, 0.1781337857246399, 0.005972636863589287, -0.021562352776527405, -0.16687169671058655, 0.07589639723300934, -0.20823828876018524, 0.027126934379339218, 0.10467753559350967, 0.06701159477233887, 0.07915465533733368, -0.024046622216701508, -0.1669970601797104, 0.07604529708623886, 0.1269170194864273, -0.21936824917793274, -0.06592322885990143, 0.06071619316935539, -0.14255106449127197, -0.047067590057849884, -0.08292384445667267, 0.2115967869758606, 0.18284666538238525, -0.15493471920490265, -0.14141127467155457, 0.15566584467887878, -0.1567707657814026, -0.005966860800981522, 0.02694620192050934, -0.14431986212730408, -0.19422967731952667, -0.27188384532928467, 0.003987520933151245, 0.2886632978916168, 0.051324617117643356, -0.24798262119293213, 0.028046492487192154, -0.03672055900096893, 0.048082903027534485, 0.10906309634447098, 0.16191940009593964, -0.008259378373622894, 0.005847998894751072, -0.11125662177801132, 0.006064308807253838, 0.1905171126127243, -0.07413583993911743, 0.02043292298913002, 0.290202260017395, 0.00569811649620533, -0.00016449671238660812, 0.11121324449777603, 0.10905371606349945, -0.09846137464046478, 0.005683856084942818, -0.15451037883758545, 0.05895839259028435, 0.04510065168142319, -0.03569173067808151, -0.06883768737316132, 0.08693848550319672, -0.16857297718524933, 0.1154068261384964, 0.007511516101658344, -0.01983277127146721, 0.02589372731745243, -0.09050130844116211, -0.020625963807106018, -0.11194785684347153, 0.08375582844018936, -0.22212722897529602, 0.17791825532913208, 0.13751709461212158, 0.0053409687243402, 0.1599988043308258, 0.060513101518154144, 0.10321187227964401, -0.030407054349780083, -0.03938911110162735, -0.22134312987327576, 0.0003300569951534271, 0.15529313683509827, 0.004012138117104769, 0.06936727464199066, -0.019267655909061432]
```

この変換を全ての画像に対して事前に行っておきます。写真と写真が類似しているかどうかは単にこのベクトルの距離が近いかどうかで判定します。ちょう簡単ですね！

ベクトルの距離は単なるユークリッド距離を用います。私は検索部分は Ruby で作ってしまったので距離の計算は適当に実装しましたが、python で検索も行う場合、face_distance というメソッドがあるのでそれも使えます。[example](https://github.com/ageitgey/face_recognition/blob/master/examples/face_distance.py)のコードをざっと眺めると良いと思います。

# インストール
Ubuntu でのやり方を解説します。

まずは python と pip が必要です。

```
apt install python3-pip
```

dlib をビルドするためには boost と cmake が必要になります。

```
apt install cmake libboost-dev libboost-python-dev
```

最期に python ライブラリをインストールします。

```
pip3 install face_recognition
```


# コード
## エンコーディング
まずは画像をベクトルにエンコーディングしましょう。次のような３行スクリプトを書いて

```py3
import face_recognition
import json
import sys
 
image = face_recognition.load_image_file(sys.argv[1])
face_encoding = face_recognition.face_encodings(image)[0]
print(json.dumps(face_encoding.tolist()))
```

実行してみます。

```
$ python3 recognize.py face.jpeg
[-0.14351621270179749, 0.057226795703172684, 0.07066182047128677, -0.13657408952713013, -0.0695628970861435, -0.09160482883453369, -0.011631922796368599, -0.14444005489349365, 0.1372034251689911, -0.12074219435453415, 0.2784915566444397, -0.1435723453760147, -0.301411509513855, -0.006434443406760693, -0.06611043959856033, 0.21539726853370667, -0.1636665165424347, -0.13245481252670288, -0.08063990622758865, 0.04853415489196777, 0.09177280962467194, -0.01833999715745449, 0.00841446127742529, 0.12095664441585541, -0.08568297326564789, -0.37953001260757446, -0.13193728029727936, -0.03719043731689453, -0.0870690569281578, -0.04294124245643616, -0.038571640849113464, 0.05095953121781349, -0.2148473709821701, -0.041665997356176376, 0.014024296775460243, 0.07775825262069702, -0.034873172640800476, -0.15043900907039642, 0.17863482236862183, -0.030670292675495148, -0.2826652228832245, 0.02874363772571087, 0.09433827549219131, 0.20609621703624725, 0.1781337857246399, 0.005972636863589287, -0.021562352776527405, -0.16687169671058655, 0.07589639723300934, -0.20823828876018524, 0.027126934379339218, 0.10467753559350967, 0.06701159477233887, 0.07915465533733368, -0.024046622216701508, -0.1669970601797104, 0.07604529708623886, 0.1269170194864273, -0.21936824917793274, -0.06592322885990143, 0.06071619316935539, -0.14255106449127197, -0.047067590057849884, -0.08292384445667267, 0.2115967869758606, 0.18284666538238525, -0.15493471920490265, -0.14141127467155457, 0.15566584467887878, -0.1567707657814026, -0.005966860800981522, 0.02694620192050934, -0.14431986212730408, -0.19422967731952667, -0.27188384532928467, 0.003987520933151245, 0.2886632978916168, 0.051324617117643356, -0.24798262119293213, 0.028046492487192154, -0.03672055900096893, 0.048082903027534485, 0.10906309634447098, 0.16191940009593964, -0.008259378373622894, 0.005847998894751072, -0.11125662177801132, 0.006064308807253838, 0.1905171126127243, -0.07413583993911743, 0.02043292298913002, 0.290202260017395, 0.00569811649620533, -0.00016449671238660812, 0.11121324449777603, 0.10905371606349945, -0.09846137464046478, 0.005683856084942818, -0.15451037883758545, 0.05895839259028435, 0.04510065168142319, -0.03569173067808151, -0.06883768737316132, 0.08693848550319672, -0.16857297718524933, 0.1154068261384964, 0.007511516101658344, -0.01983277127146721, 0.02589372731745243, -0.09050130844116211, -0.020625963807106018, -0.11194785684347153, 0.08375582844018936, -0.22212722897529602, 0.17791825532913208, 0.13751709461212158, 0.0053409687243402, 0.1599988043308258, 0.060513101518154144, 0.10321187227964401, -0.030407054349780083, -0.03938911110162735, -0.22134312987327576, 0.0003300569951534271, 0.15529313683509827, 0.004012138117104769, 0.06936727464199066, -0.019267655909061432]
```

あら簡単！これを全ての画像に実行しておいてデータベースなどに保存しておきます。

## 検索
python には慣れていないというかプロダクトのコードが Ruby ベースなので検索部分は Ruby で作りました。
とは言っても本質的には以下の様なことをやるだけです。

検索したい画像が与えられたらまず上記pythonプログラムでベクトルに変換し、次のメソッドに渡します。
簡易バージョンでは all_vectors は予め全てのベクトルをメモリに読み込んでおきました。

```
def search(v)
  @all_vectors.min_by |u|
    euclidean_distance(u,v)
  end
end
```

上位１０位を返すようにするなどカスタマイズは容易だと思います。


# 結果
結果の写真を貼り付けれると良いのですが、サービスユーザの顔写真しかないのでお見せできません。

個人的な感想としては半日で作った機能にしてはよくできている、という感じです。
同一人物では？と思うレベルで似ている顔をサジェストしてくることもあれば、あまり似ていない顔をサジェストしてくることもあります。
30,000枚ほどの画像を使いましたが、だいたい７割くらいの確率で似ていると感じることのできる画像がサジェストされました。
まだこういう機能は物珍しく、狩野モデルでいうところの魅力品質に相当すると思うので結果の全てが似ている必要はなく、そういう意味で自社のサービスでは十分実践投入できるレベルだと感じました。

当然といえば当然なのかもしれませんが、異なる性別の画像がサジェストされることはあまりありませんでした。

画像の枚数をもっと増やすことで精度は上がっていくと思われます。

## うまくいく時
顔がとても整っている、顔の輪郭が丸い、目が大きいなど特徴のある顔の場合はうまくいくように思います。

## うまくいかない時
まず以下のような場合はそもそも顔が認識されませんでした。
* 顔に強く影が乗っている
* 写真がぼやけている
* 顔が正面を向いていない

顔が小さく写っている写真はかなり適当なエンコーディングがされるようで、ノイズとなりますので除去したほうが良いです。
そのような画像の特定も簡単にできます。（face_locationsというメソッドを使います）

# 制限
このライブラリは冒頭に書いたように顔のランドマークの情報のみを特徴として抽出し、それを比較するだけの機能です。
これはつまり以下のような情報だけを見ていることになります。

* 各パーツの位置関係
* 各パーツの形・大きさ
* 顔の輪郭

一方で以下のような情報は使っていません。

* 髪型
* 肌の色・皺・テクスチャ
* 鼻の詳細な形（鼻の穴の大きさや太さなど）
* まぶた（一重、二重）
* 耳の形

このような詳細な情報を用いた類似検索を行いたい場合はディープラーニングを使う必要があるのかなと思います。

# 高速化
上記の例では単純に全部の画像を比較する線形探索を行いましたが、これでは画像の数が 10,000 程度になった時点で使い物にならなくなります。

https://ekzhu.github.io/datasketch/lshforest.html

このような確率的近似ライブラリを使うことで高速に検索ができるようになります。