FaceNetの顔認証をお手軽に試す

# はじめに
この記事は[顔学2020アドベントカレンダー](https://qiita.com/advent-calendar/2020/facial_study)の5日目の記事です．
今日はFaceNetを手軽に試せるようにラップしてくれているPythonのライブラリであるkeras-facenetの使い方をまとめようと思います．顔学会には工学に詳しくない方も多いので，なるべく込み入った内容には触れずに，ただコードを実行すれば再現できるように進めていくつもりです．

# FaceNetとは
[FaceNet](https://github.com/davidsandberg/facenet)はGoogleが開発した顔認証のための顔認識モデルです．Inception ResNet V1というモデルの出力にL2ノルムを取った結果を顔の特徴量ベクトルと考えるモデルです．深層学習による顔空間の構築とも言えるかもしれません．

顔認証の流れとしては，

- FaceNetに顔画像を入力して顔ベクトルを取得する
- 顔ベクトル同士のユークリッド距離を計算する
- 距離が閾値以下なら同一人物とみなす

以上の三つだけです．この工程を順番にコードを見ながら追っていきます．

![facenet_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593087/31c95598-a704-288f-1103-4d6524ecebb8.png)


# データセット
まず，今回使用する顔画像については安倍前首相・菅現首相の顔画像をそれぞれ5枚を用意しました．Google Driveで公開していますので，ダウンロード可能です．

```console
> wget "https://drive.google.com/uc?export=download&id=1toQim1h0fX_IkRAnCpNZYXsrBmqba1QS" -O images.zip
> unzip images.zip
```

# FaceNetを使った顔認証
[Google Colab](https://colab.research.google.com/drive/1SnBvOmDVa_KN6SqbhJJK90yvMddASZmp?usp=sharing)で実装したコードと実行結果を載せていきます．

## ライブラリのインストール
今回はkeras-facenetを使って簡単にFaceNetを利用します．miraはMTCNNという顔領域検出モデルを利用するためにインストールしています．

```console
> !pip install mira keras-facenet
```

## 顔画像から顔ベクトルを取得する
安倍さんと菅さんの顔画像をMTCNNという顔領域検出モデルを用いて切り抜き，FaceNetに通して顔ベクトルに変換します．その時に使った顔領域画像も後ほど使うので取っておきます．

```python
from mira.detectors import MTCNN
from keras_facenet import FaceNet
from PIL import Image
import numpy as np
import cv2
import matplotlib.pyplot as plt
import os

dirs = ["images/abe",  "images/suga"]

embeddings = [] # 顔ベクトル（顔特徴量）
imgs = [] # 顔領域を切り取ったもの
detector = MTCNN() # 顔領域の検出器
embedder = FaceNet() # FaceNetモデル

for dir in dirs:
  files = os.listdir(dir) # ディレクトリ のファイルリストを取得
  for file in files:
    file_path = os.path.join(dir, file)
    print(file_path)
    img = cv2.imread(file_path) # 画像読み込み
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # RGB形式に変換
    faces = detector.detect(img_rgb) # 顔領域を検出．画像中に複数の顔が検出されることも想定する
    embedding = embedder.embeddings([face.selection.extract(img_rgb) for face in faces]) # 潜在変数表現に変換
    embeddings.append(embedding[0])
    imgs.append(faces[0].selection.extract(img_rgb)) # 顔領域を保存しておく
```

## 顔ベクトルの様子を見る
ひとまず顔ベクトルがどんな具合に取れているか確認してみます．とはいえFaceNetで展開される顔空間は512次元なので，ひとまず主成分分析（PCA）で2次元まで次元削減して可視化します．また，圧縮前の特徴量にK-meansというクラスタリング手法を適用して10件のサンプルを2クラスに分類してプロットします．正しく顔ベクトルが取れているならただしく分類された結果がプロットされるはずです．

### クラスタリングとPCA

```python
from sklearn.cluster import KMeans
K = 2 # クラスタ数
kmeans = KMeans(n_clusters=K).fit(embeddings) # 圧縮前にクラスタリングしておく
pred_label = kmeans.predict(embeddings)

from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pca.fit(embeddings)
reduced_embeddings = pca.fit_transform(embeddings)
```

### 可視化

```python
import matplotlib.pyplot as plt
%matplotlib inline
# 結果をプロット
x = reduced_embeddings[:, 0]
y = reduced_embeddings[:, 1]
plt.scatter(x, y)
plt.show()
```

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593087/a41f20c1-c115-9aa2-143d-939d48eb4b7f.png)

プロットされた座標をみてみるとしっかり左右に分離できていました．主に横軸になっている第一主成分だけで安倍さんと菅さんの違いは認識できているようですね．

### 顔画像を使って可視化
もう少し視覚的にわかりやすくするために，[こちらの記事](https://qiita.com/yottyann1221/items/c0b2d2591c3e9ce27c84)で使われていた関数を少し書き換えて顔画像で散布図を作成します．

```python
from matplotlib.offsetbox import OffsetImage, AnnotationBbox 
def imscatter(x, y, imgs, ax=None, zoom=1): 
    if ax is None: 
        ax = plt.gca() 

    artists = [] 
    for x0, y0, img in zip(x, y, imgs): 
        im = OffsetImage(img, zoom=zoom) 
        ab = AnnotationBbox(im, (x0, y0), xycoords='data', frameon=False) 
        artists.append(ax.add_artist(ab)) 
    return artists

x = reduced_embeddings[:, 0]
y = reduced_embeddings[:, 1]
fig, ax = plt.subplots() 
imscatter(x, y, imgs, ax=ax,  zoom=0.2) 
ax.plot(x, y, 'ko',alpha=0) 
ax.autoscale() 
plt.show() 
```

入力画像サイズを統一していなかったので若干みづらいですが，顔空間の概要を掴みやすくなりました．
![scatter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593087/2711ad22-b2b0-db77-828c-83a6b84d9cde.png)

### ユークリッド距離の計算
顔ベクトル同士のユークリッド距離を計算し，その得られた距離が大きいかどうかで同一人物かどうかを判定します．scipyで簡単に計算することができます．
ここでは，まず同一人物の時とそうでない時でユークリッド距離にどれほどの差が出るのかを総当たりで計算します．

```python
import scipy
from scipy import spatial

# ユークリッド距離を計算
def calc_euclid_distance(a, b):
  return scipy.spatial.distance.euclidean(a, b)

abes = embeddings[:5]
sugas = embeddings[5:10]

# 違う人物の場合のユークリッド距離
negatives = []
for abe in abes:
  for suga in sugas:
    negatives.append(calc_euclid_distance(abe, suga))

# 同一人物の場合のユークリッド距離
positives = []
for i in range(5):
  for j in range(5 - (i + 1)):
    positives.append(calc_euclid_distance(abe[i], abe[i + j + 1]))
    positives.append(calc_euclid_distance(suga[i], suga[i + j + 1]))

print(np.average(positives)) # 0.04065310596488416
print(np.average(negatives)) # 1.0330342292785644
```

同一人物の場合は平均で0.04の距離であり，逆に違う人物の場合は1.03程度の距離があることがわかりました．なので，閾値は0.5程度で設定すれば間違いないでしょうか．
実際に論文を読んでみると公式には1.1の閾値を使用していましたが，今回はそれでは判別できませんね．おそらくデータセットには日本人のデータが少なかったのでしょう．日本人という雑なカテゴリで分類されていたりして，距離が近くなりやすいんだと思います．用途によって個別に設定する必要があるみたいですね．

それにしても，日本人でもそこそこちゃんと分離できているのはすごいですね．

# さいごに
今日は顔認証モデルのFaceNetをお手軽に試せるライブラリであるkeras-facenetを紹介しました．アドベントカレンダーの言い出しっぺなので結構頑張って記事を書いていますが，そろそろ修論が本格的にやばいのでしばらく記事のほうはお休みさせていただきます．もしみなさんも顔に関する知識や経験がありましたら，アドベントカレンダーに登録をお願いします．

コードはこちらで公開しているので是非試してみてください．
https://colab.research.google.com/drive/1SnBvOmDVa_KN6SqbhJJK90yvMddASZmp?usp=sharing

それでは．

# 追記
FaceNetを使った面白そうな研究を発見したので共有．

[FaceNetに対するAdversarial Exampleによる意図的誤認識](https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_action_common_download&item_id=181390&item_no=1&attribute_id=1&file_no=1)

FaceNetのモデルに対してAdversarial Exampleを入力して誤作動を起こさせた結果をまとめた論文．
Adversarial Exampleは正しく認識できていた画像に対して，勾配上昇するようにノイズをのせてネットワークに誤認識を起こさせる攻撃手法です．人間が見たらほぼ同じ画像ですが，ネットワークの判断を大幅に誤らせることができます．

![panda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593087/4d4dd151-2b54-8737-390a-e7cabc0b050c.png)

[Explaining and Harnessing Adversarial Examples](https://arxiv.org/abs/1412.6572)



# 参考
- [FaceNet](https://github.com/davidsandberg/facenet)
- https://qiita.com/yottyann1221/items/c0b2d2591c3e9ce27c84
- https://qiita.com/zeze/items/09bdd7229713c0a6b090
- https://qiita.com/deaikei/items/ecbf2e796e771bf7113f
- [FaceNetに対するAdversarial Exampleによる意図的誤認識](https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_action_common_download&item_id=181390&item_no=1&attribute_id=1&file_no=1)
- [Explaining and Harnessing Adversarial Examples](https://arxiv.org/abs/1412.6572)