ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

顔認証におけるいろいろな損失関数(Loss function)

こんにちは、iOSエンジニアのしだです。いつの間にかR&Dチームとして1年経ってしまいました。R&Dというと社内でも何をやっているか理解しづらい部署であり、いよいよ社内でもぼっち感がでてきました。 ここ最近は、顔認証(Face Recognition)ばかりやっていて、iOSからも同僚からもだいぶ離れてしまいさびしさも感じられる今日このごろです。 今回は、顔認証におけるディープラーニングで学習する際に用いられる損失関数についてどのような違いがあるか見てみたいと思います。

損失関数(Loss function)

顔認証は、この顔とあの顔が同一人物かどうか、顔の特徴量を使って同定することです。 この顔の特徴量は、ディープラーニングを用いて学習して、最終的に埋め込みベクトル化(Embeddings)することが多いです。 この顔の特徴ベクトルをうまく学習するための損失関数には、通常の分類タクスで用いられる Softmax Lossから、Triplet LossCenter Loss、Marginal Loss、SphereFaceCosFaceArcFaceなどなどいろいろな学習のやり方があります。 Identificationタスクであれば、Softmax LossによるEmbeddingでも十分な場合もありそうですが、Verificationタスクにおいて、高い角度でその顔をその顔だと同定するための工夫がされています。 今回はMNISTのデータセットを使って、いくつかの損失関数で学習した結果を可視化してみたいと思います。

準備

  • Python: 3.5.2
  • TensorFlow: 1.12.0
  • TensorBoard: 1.12.0

MNIST画像の埋め込みベクトル化

MNISTを使って各損失関数の特性を可視化したいと思います。各損失関数の実装確認をするためにMNISTを使っていたので、それを今回TensorBoardを使って可視化してみた感じです。 各損失関数で学習させるモデルは4層のCNNで、出力が3次元にEmbeddingされるように学習させています。最後にテスト用のMNIST画像から3次元ベクトル化してTensorBoard上にプロットしています。

f:id:unifa_tech:20190205180749p:plain:w360

以下、Softmax Loss、Center Loss、AM Softmax、ArcFace の4つの方法を試しました。Triplet LossやMarginal Lossはミニバッチ時にペアの取り方やクラスと枚数を均等に取るのがちょっと大変そうだったので諦めました😭

Softmax Loss

ここではFullConnectではなく、バイアスなし、埋め込みベクトルと重みをL2 Normalizationしてから積を取ります。これで同一球面上に分布するように埋め込みベクトルが学習されます。

\displaystyle \mathcal { L_{softmax} } = - \frac { 1 } { N } \sum _ { i = 1 } ^ { N } \log \frac { e ^ { \left| W _ { j } \right| \left| x _ { i } \right| \cos \theta _ { j } } } { \sum _ { j = 1 } ^ { n } e ^ { \left| W _ { j } \right| \left| x _ { i } \right| \cos \theta _ { j } } }

損失関数の実装

def softmax(embeddings, labels, num_classes):
    embedding_size = embeddings.get_shape()[1]
    with tf.name_scope('softmax'):
        kernel = tf.get_variable(
            name='kernel', shape=[embedding_size, num_classes],
            initializer=tf.contrib.layers.xavier_initializer(), 
            dtype=tf.float32)
        kernel = tf.nn.l2_normalize(kernel, 0, 1e-10, name='kernel_norm')
        original_target_logits = tf.matmul(embeddings, kernel, name='original_target_logits')
    return original_target_logits

Embeddingsのプロット

Center Loss

Center Lossは、Softmaxの項にクラスとサンプル間を小さくするための { L } _ { C }の項を追加してよりクラス内のばらつきを小さくなるようにLossを計算しています。

\displaystyle \mathcal { L } = \mathcal { L } _ { S } + \lambda \mathcal { L } _ { C } = - \sum _ { i = 1 } ^ { m } \log \frac { e ^ { W _ { y _ { i } } ^ { T } \boldsymbol { x } _ { i } + b _ { y _ { i } } } } { \sum _ { j = 1 } ^ { n } e ^ { W _ { j } ^ { T } \boldsymbol { x } _ { i } + b _ { j } } } + \frac { \lambda } { 2 } \sum _ { i = 1 } ^ { m } \left| \boldsymbol { x } _ { i } - \boldsymbol { c } _ { y _ { i } } \right| _ { 2 } ^ { 2 }

 Y. Wen, K. Zhang, Z. Li, and Y. Qiao, A discriminative feature learning approach for deep face recognition https://link.springer.com/chapter/10.1007/978-3-319-46478-7_31

損失関数の実装

def center_loss(features, labels, num_classes, alpha=0.5):
    with tf.variable_scope('center_loss'):
        embedding_size = features.get_shape()[1]
        centers = tf.get_variable(
            'centers', [num_classes, embedding_size], dtype=tf.float32,
            initializer=tf.constant_initializer(0), trainable=False)
        labels = tf.reshape(labels, [-1])
        center_batch = tf.gather(centers, labels)
        
        # ⊿c の手動更新
        _, unique_index, unique_count = tf.unique_with_counts(labels)
        times = tf.reshape(tf.gather(unique_count, unique_index), [-1, 1])
        delta = (center_batch - features) / tf.cast(1 + times, tf.float32)
        delta = alpha * delta
        centers_op = tf.scatter_sub(centers, labels, delta)
        centers_op = tf.nn.l2_normalize(centers_op)

        with tf.control_dependencies([centers_op]):
            # loss = tf.nn.l2_loss(features - center_batch, name='center_l2_loss')
            loss = tf.losses.mean_squared_error(center_batch, features)
        return loss, centers_op

davidsandberg/facenetのCenter Lossの実装を参考にさせてもらいました。)

Embeddingsのプロット

AM Softmax(Additive Margin Softmax Loss)

Softmax Loss に示した式に近いですが、基本的な考え方は、 \cos \theta に対してマージンという名のペナルティを課してSoftmaxのお力を借りてIntra-Classを小さくしようということです。

\displaystyle \mathcal { L } _ { ams } = - \frac { 1 } { n } \sum _ { i = 1 } ^ { n } \log \frac { e ^ { s \cdot \left( \cos \theta _ { y _ { i } } - m \right) } } { e ^ { s \cdot \left( \cos \theta _ { y _ { i } } - m \right) } + \sum _ { j = 1 , j \neq y _ { i } } ^ { c } e ^ { s \cdot \cos \theta _ { j } } }

 F. Wang, W. Liu, H. Liu, and J. Cheng, Additive Margin Softmax for Face Verification https://arxiv.org/abs/1801.05599

損失関数の実装

def am_softmax(embeddings, labels, num_classes, m=0.5, s=64):
    embedding_size = embeddings.get_shape()[1]
    with tf.name_scope('cosface'):
        kernel = tf.get_variable(
            name='kernel',
            shape=[embedding_size, num_classes],
            initializer=tf.contrib.layers.xavier_initializer(),
            dtype=tf.float32)
        kernel = tf.nn.l2_normalize(kernel, 0, 1e-10, name='kernel_norm')

        original_logits = tf.matmul(embeddings, kernel, name='original_logits')
        one_hot = tf.one_hot(labels, num_classes)

        condition = tf.logical_and(tf.equal(one_hot, 1), tf.cast(original_logits > m, tf.bool))
        logits = tf.where(condition, original_logits - m, original_logits)
        logits = s * logits
        return logits

Embeddingsのプロット

ArcFace(Additive Angular Margin Loss)

AM Softmaxも ArcFace も基本的には同じなのですがマージンの取り方が異なり、 \theta に対してマージンを取ります。ArcFaceの論文内にある擬似コードをもとにそのままTensorFlowで実装してます。

\displaystyle L _ { arcface } = - \frac { 1 } { N } \sum _ { i = 1 } ^ { N } \log \frac { e ^ { s \left( \cos \left( \theta _ { y _ { i } } + m \right) \right) } } { e ^ { s \left( \cos \left( \theta _ { y _ { i } } + m \right) \right) } + \sum _ { j = 1 , j \neq y _ { i } } ^ { n } e ^ { s \cos \theta _ { j } } }

 J. Deng, J. Guo, and S. Zafeiriou, ArcFace: Additive Angular Margin Loss for Deep Face Recognition https://arxiv.org/abs/1801.07698

損失関数の実装

def arcface(embeddings, labels, num_classes, m=0.5, s=64):
    embedding_size = embeddings.get_shape()[1]
    with tf.name_scope('arcface'):
        kernel = tf.get_variable(
            name='kernel',
            shape=[embedding_size, num_classes],
            initializer=tf.contrib.layers.xavier_initializer(),
            dtype=tf.float32)
        kernel = tf.nn.l2_normalize(kernel, 0, 1e-10, name='kernel_norm')
        original_target_logits = tf.matmul(embeddings, kernel, name='original_target_logits')

        # -1, 1 のときにacosを取ると微分時にinfになってしまうので -1 < x < 1 になるようにクリップする
        z = 1 - 1e-6
        original_target_logits = tf.clip_by_value(original_target_logits, -z, z)

        theta = tf.acos(original_target_logits)
        threshold = original_target_logits - tf.cos(m)
        
        one_hot = tf.one_hot(labels, num_classes)
        marginal_target_logits = tf.where(tf.cast(threshold > 0, tf.bool), tf.cos(theta+m), tf.cos(theta))
        diff = tf.subtract(marginal_target_logits, original_target_logits)
        logits = original_target_logits + tf.multiply(one_hot, diff)
        return s * logits

Embeddingsのプロット

さいごに

今回、Softmax Loss、Center Loss、AM Softmax、ArcFaceの4つの手法を用いて、MNISTの埋め込みベクトルをプロットしてみました。この中ではArcFaceが一番キュッと集まっているように見えます。ハイパーパラメータもそんなに変えて試していないので他の手法も、もう少し改善するかもしれません。ArcFaceの論文の顔画像データセット行った際も結果が良いので、顔画像のデータセットでも学習させてみようかなと思います。

参考