ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

FaceNet の学習済みモデルを使って顔画像のクラスタリングを行う

お久しぶりになります、iOSエンジニアのしだです。
開発部ブログも更新する人が多くなって、前回からだいぶ日が空いたように感じます。

今回は、あるまとまった写真から顔を切り出して、FaceNetから特徴量を抽出しクラスタリングを試しました。
以下の FaceNet のトレーニング済みのモデルがあるので使用しています。 FaceNet は、Triplet Loss と呼ばれる基準となる Anchor と Positive の距離を近くに、Negative との距離を遠くにマッピングされるように学習されます。 顔画像から特徴ベクトルとして利用可能なのでクラスタリングなどのタスクに利用できます。

github.com

準備

  • Python 3.6.6
  • FaceNet リポジトリを clone しておく
$ mkdir facenet-example
$ cd facenet-example
$ git clone https://github.com/davidsandberg/facenet.git
  • FaceNet の学習済みモデル から学習済みモデルをダウンロード
  • 顔画像: 112枚(81枚の家族写真素材 から顔検出して切り出したもの)

f:id:unifa_tech:20180920124240p:plain:w280

特徴ベクトルの抽出

FaceNet の facenet/src/compare.py に実際のサンプルコードがあるので、そちらを見てもらうほうが理解しやすいかもしれません。 FaceNet のモデルを利用するためのサンプルコードです。グラフと tf.Session を再利用できるように変えています。

from facenet.src import facenet

class FaceEmbedding(object):

    def __init__(self, model_path):
        # モデルを読み込んでグラフに展開
        facenet.load_model(model_path)
        
        self.input_image_size = 160
        self.sess = tf.Session()
        self.images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0")
        self.embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
        self.phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0")
        self.embedding_size = self.embeddings.get_shape()[1]
        
    def __del__(self):
        self.sess.close()
        
    def load_image(self, image_path, width, height, mode):
        image = Image.open(image_path)
        image = image.resize([width, height], Image.BILINEAR)
        return np.array(image.convert(mode))
        
    def face_embeddings(self, image_path):
        image = self.load_image(image_path, self.input_image_size, self.input_image_size, 'RGB')
        prewhitened = facenet.prewhiten(image)
        prewhitened = prewhitened.reshape(-1, prewhitened.shape[0], prewhitened.shape[1], prewhitened.shape[2])
        feed_dict = { self.images_placeholder: prewhitened, self.phase_train_placeholder: False }
        embeddings = self.sess.run(self.embeddings, feed_dict=feed_dict)
        return embeddings

使い方

# ダウンロードした学習済みのモデルを解凍してパスを指定します
FACE_MEDEL_PATH = './models/20180402-114759/20180402-114759.pb'
face_embedding = FaceEmbedding(FACE_MEDEL_PATH)

# 顔画像のファイルリスト
faces_image_paths = glob.glob('./images/faces/*.jpg')

# 顔画像から特徴ベクトルを抽出
features = np.array([face_embedding.face_embeddings(f)[0] for f in faces_image_paths])
print(features.shape) # ==> (112, 128) 顔画像112枚が128次元表現になります

クラスタリング

PCA を使って次元削減

128次元の特徴ベクトルを2次元座標にプロットするために、128次元から2次元にPCAを使って次元削減します。 scikit-learn を用いれば容易にPCA を利用できます。

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
pca.fit(features)
reduced = pca.fit_transform(features)
print(reduced.shape) # ==> (112, 2) 128次元を2次元表現にされます
print(pca.explained_variance_ratio_) # ==> array([0.21991092, 0.13256498], dtype=float32)

kMeans によるクラスタリング

次元削減した2次元ベクトルを kMeans を使って、4つにクラスタリングしてみます。
4つにしたのは、使用した家族写真素材に4人写っていることを目視で確認して今回はクラスター数4に指定しています。

from sklearn.cluster import KMeans

K = 4
kmeans = KMeans(n_clusters=K).fit(reduced)
pred_label = kmeans.predict(reduced)
print(pred_label) # ==> [2 0 2 1 1 1 1 2 1 ...] 


# クラスタリングした結果をプロット
x = reduced[:, 0]
y = reduced[:, 1]
plt.scatter(x, y, c=pred_label)
plt.colorbar()
plt.show()

f:id:unifa_tech:20180920142248p:plain:w280

あとは、実際にクラスタリングされた各クラスターが、同じような顔画像の集まりになっているか確認します。
少し間違っている画像もありますが、おおよそ合っていそうです。 子どもの顔画像(赤点、緑点)は割と近い分布になっており、お父さん・お母さんぽい顔画像は離れて分布されているため、大人の顔画像は判別しやすい印象があります。

f:id:unifa_tech:20180920142522p:plain:w560

さいごに

学習済みの FaceNet が簡単に利用できるので、お試しで顔認識するときはとても重宝してます。 顔認識は弊社でも利用用途が多いのでこの当たりの技術はチェックしていきたいと思います。