기존의 딥러닝 기반 얼굴 인식 연구는 두 가지 방법으로 정리할 수 있습니다.
첫번째 방식은 softmax loss를 사용하는 방식으로 분류 모델을 softmax를 통해 훈련하는 방식입니다. 두번째는 triplet loss를 사용하는 방식인데 이 방식은 분류 모델을 학습하는 것이 아닌 임베딩 벡터를 학습하는 방식입니다.
저자들은 두 loss 모두 단점이 몇 가지 존재한다고 설명합니다. softmax loss의 경우 가지고 있는 훈련 데이터셋에 적합한 학습을 하기 때문에 open-set에는 적합하지 않다고 설명을 하고 있는데, 이는 쉽게 말해 새로운 인물에 대해서는 적절한 feature를 추출하지 못하는 단점을 가지고 있다고 보시면 됩니다. triplet loss의 경우 학습에 어려움이 있다는 점과, face triplet의 조합이 폭발적으로 증가한다는 단점이 있다고 합니다. 이러한 기존 loss들의 단점을 해결하기 위해 저자들이 새롭게 내놓은 loss가 바로 ArcFace, 또는 Additive AngularMargin Loss라고 불리는 loss입니다.
Softmax Loss
softmax loss는 다음과 같은 수식으로 나타낼 수 있습니다. 갑자기 왠 Weight, bias 등이 있어서 뭔가 싶을 수 있으니 조금 더 살펴보도록 하겠습니다.
$$ L_1 = -{1\over N}\sum{log{e^{W^T_{y_i}x_i+b_{y_i}}\over{\sum^n_{j=1}{e^{W^T_jx_i+b_{j}}}}}} $$
우선 softmax에 대해 크로스엔트로피를 적용한 수식이 softmax-loss입니다. 아래의 수식과 같습니다.
$$ L = {1\over N}\sum{L_i} = {1\over N}\sum{-\log({e^f_{y_i}\over\sum_j{e^f_j}}}) $$
여기에서 $f$는 fully-connect-layer의 output을 의미합니다. CNN 등을 통해 나온 최종 임베딩 벡터 $x_i$에 대해 학습을 위한 레이어 $f$를 추가할 경우, 가중치 벡터 $W$를 곱한 후 Bias $b$를 더한 수식이 나오게 되므로, $f$부분을 $W^Tx_i + b$로 표현할 수 있는데, 이를 적용하면 위의 softmax loss, $L_1$식이 나오게 됩니다.
저자들은 이 softmax loss는 Feature Embedding을 최적화하지는 않는 loss이기 때문에, 클래스 내의 큰 외모 변화(포즈 변화) 등의 문제에 유연하지 않다고 설명하고 있습니다.
ArcFace
ArcFace는 위의 softmax loss를 변형한 새로운 loss입니다.
우선 $f$, fully-connect-layer의 output은 $W^Tx_i + b$ 입니다. 여기서 $b$를 0으로 두어 없애보도록 하겠습니다.
여기서 $W^T$가 각 클래스의 대표 벡터를 의미한다고 보면, 이 연산의 결과는 $W^T$(각 클래스의 대표 벡터)와 $x_i$(임베딩 벡터)의 내적 유사도라고 볼 수 있습니다.
이 식을 softmax loss에 적용하면 다음과 같이 정리할 수 있습니다.
$$ L = -{1\over N}\sum{log{e^{W^T_{y_i}x_i}\over{\sum^n_{j=1}{e^{W^T_jx_i}}}}} $$
이를 해석하면 정답 클래스의 대표 벡터와 임베딩 벡터 간의 유사도 계산이, 정답 외의 다른 클래스의 유사도 계산보다 커질 수 있게 학습을 하는 loss로 사용할 수 있다는 뜻입니다.
이 식에 대해 내적의 기하학적 정의인 $a*b = |a||b|\cos\theta$ 를 사용하면 j번째 클래스에 대한 $W^T_jx_i$를 아래와 같은 수식으로 정리할 수 있습니다.
$$ W^T_jx_i = |W_j||x_i|\cos\theta_j $$
여기에 저자들은 $|W_j| = 1$로 정규화하고, $|x_i| = s$로 정규화를 수행합니다. 정확하게는 $|W_j|$, $|x_i|$ 모두 L2 norm을 적용하여 1로 정규화 한 후, $|x_i|$는 $s$로 rescaling합니다.
이렇게 내적의 기하학적 정의와 정규화를 적용한 softmax loss는 다음과 같이 정리할 수 있습니다.
$$ L_2 = -{1\over N}\sum{log{e^{s\cos\theta_{y_i}}\over{e^{s\cos\theta_{y_i}}+\sum^n_{j=1,j\not=y_i}{e^{s\cos\theta_j}}}}} $$
이 식의 의미는 클래스 각각의 대표 벡터들과 임베딩 벡터 사이의 각을 이용하여 만든 새로운 임베딩 공간을 학습한다고 볼 수 있습니다.
여기에 저자는 실제 해당 클래스 레이블을 가지고 있는 각 $\theta_{y_i}$에 대해서만 angular margin penalty $m$을 더했습니다. 이렇게 나온 최종 결과값이 Additive Angular Margin Loss, ArcFace입니다.
$$ L_3 = -{1\over N}\sum{log{e^{s(\cos(\theta_{y_i}+m))}\over{e^{s(\cos(\theta_{y_i}+m))}+\sum^n_{j=1,j\not=y_i}{e^{s\cos\theta_j}}}}} $$
위의 $L_2$식에서부터 갑자기 분모에 $\theta_{y_i}$관련 식이 빠져나와 있던 이유는 margin을 따로 붙이기 위함입니다.
softmax loss를 변형하여 얻어낸 최종 식, ArcFace는 다음과 같이 설명할 수 있습니다.
정답 클래스의 대표 벡터 $W_{y_i}$, 정답이 아닌 클래스의 대표 벡터 $W_{j\not=y_i}$, 그리고 지금 들어온 이미지의 임베딩 벡터 $x_i$(위 그림의 $f_\theta(Xi)$)가 있다고 할 때,
이미지와 정답 간의 거리는 $\theta_{y_i,i}$로 표현할 수 있고, 정답이 아닌 벡터와의 거리는 $\theta_{j,i}$라고 볼 수 있습니다.
ArcFace는 $\theta_{j,i}$가 $\theta_{y_i,i} + m$보다 크도록 학습이 수행되므로, intra-class끼리는 굉장히 가까워지도록, inter-class끼리는 멀어지도록 학습이 된다고 이해할 수 있습니다.
Code
이론을 이해했으니 이제 코드를 구현해보도록 하겠습니다. 게시물에 올린 코드는 tensorflow 코드지만, 하기 참조에 있는 pytorch 코드도 구조는 동일합니다.(easy_margin 정도만 추가)
참고로 아래의 ArcFace 코드의 output은 $s * \cos\theta$ 입니다. 여기서 나온 output을 softmax loss의 input으로 넣어서 학습한다고 보시면 됩니다.
# tensorflow
class ArcMarginPenaltyLogists(tf.keras.layers.Layer):
"""ArcMarginPenaltyLogists"""
def __init__(self, num_classes, margin=0.5, logist_scale=64, **kwargs):
super(ArcMarginPenaltyLogists, self).__init__(**kwargs)
self.num_classes = num_classes
self.margin = margin
self.logist_scale = logist_scale
def build(self, input_shape):
self.w = self.add_variable(
"weights", shape=[int(input_shape[-1]), self.num_classes])
self.cos_m = tf.identity(math.cos(self.margin), name='cos_m')
self.sin_m = tf.identity(math.sin(self.margin), name='sin_m')
# [아래 설명 참고]
self.th = tf.identity(math.cos(math.pi - self.margin), name='th')
self.mm = tf.multiply(self.sin_m, self.margin, name='mm')
def call(self, embds, labels):
# l2 norm
normed_embds = tf.nn.l2_normalize(embds, axis=1, name='normed_embd')
normed_w = tf.nn.l2_normalize(self.w, axis=0, name='normed_weights')
# Wx + b(b=0)
cos_t = tf.matmul(normed_embds, normed_w, name='cos_t')
# sin_theta 계산(cos^2 + sin^2 = 1)
sin_t = tf.sqrt(1. - cos_t ** 2, name='sin_t')
# cos(theta + m) 계산
'''
cos(x + y) = cos(x) * cos(y) - sin(x) * sin(y)
'''
cos_mt = tf.subtract(
cos_t * self.cos_m, sin_t * self.sin_m, name='cos_mt')
# [아래 설명 참고]
'''조건부로 cos_mt, cos_t - self.mm을 나눠서 적용'''
cos_mt = tf.where(cos_t > self.th, cos_mt, cos_t - self.mm)
# masking
'''
output값들 중에는 cos_theta만 적용해야 하는 경우가 있기 때문
'''
mask = tf.one_hot(tf.cast(labels, tf.int32), depth=self.num_classes,
name='one_hot_mask')
# masking에 따라서 cos(theta), cos(theta+margin)을 따로 적용
logists = tf.where(mask == 1., cos_mt, cos_t)
logists = tf.multiply(logists, self.logist_scale, 'arcface_logist')
return logists
line마다 주석을 추가해두었습니다. 주석을 읽으면서 보시면 편하게 이해하실 수 있습니다.
다만 논문에서 다루지 않았던 코드들이 있는데요, 이에 대해서 조금 설명을 해 보도록 하겠습니다.
우선 이러한 코드들이 있는 이유는 코사인 함수 때문입니다.
코사인 함수는 (0, pi)까지는 단조감소 함수로 사용을 할 수 있기 때문에, 이후 값을 제한해야만 합니다.
따라서, cos_mt = tf.where(cos_t > self.th, cos_mt, cos_t - self.mm)
식은 세타값이 파이에서 마진을 뺀 값보다 작을 경우(코사인을 적용했기 때문에 부호가 반대) 제한을 추가한 값으로 대치하기 위함입니다.
이제 cos_mt
와 self.th
쪽 식은 설명이 되었습니다. 그럼 self.mm
이 식은 무엇일까요?
만약 theta + margin이 pi보다 클 경우, 이를 대치하는 방법은 간단하게는 cos(theta+margin)을 -1로 고정시키는 방법이 있을 수 있습니다. 다른 방법으로는 테일러 급수를 활용한 근사 방법이 있는데, self.mm
은 테일러 급수를 활용하기 위한 값입니다. 이에 대한 설명은 아래 블로그에 자세히 적혀 있습니다. https://mic97.tistory.com/29
참고
[Open DMQA Seminar] Deep Metric Learning
Face_Pytorch/ArcMarginProduct.py at master · wujiyang/Face_Pytorch