人臉識別損失函式簡介與Pytorch實現:ArcFace、SphereFace、CosFace
一般來說,人臉識別分三步走:
找人臉:圖片中找出含人臉的區域框出來
對齊人臉:將人臉的眼鏡鼻子嘴巴等標出來,以此作為依據對齊人臉
識別:將對齊的人臉進行識別,判定這張臉究竟是誰
本篇要介紹的損失函式,用於第三步驟,聚焦於更準確地識別這張臉究竟屬於誰,本質上屬於一個分類問題。
一言以蔽之ArcFace、SphereFace、CosFace三個損失函式相對於前輩們而言,改進的一個核心思路就是:
只有平常(train)更刻苦的訓練,才有可能在比賽中(test)中得到更好的結果。
它們都對卷積神經網路提出了更高的目標,在訓練階段更為艱難,也因此讓其成為了一個更好的分類器。
一、從前輩說起
首先談談他們的前輩:
維基百科介紹:
Softmax函式
,或稱
歸一化指數函式
[1],是邏輯函式的一種推廣。它能將一個含任意實數的K維向量
“壓縮”到另一個K維實向量
中,使得每一個元素的範圍都在
之間,並且所有元素的和為1。該函式的形式通常按下面的式子給出:
簡單來說 softmax 將一組向量進行壓縮,使得到的向量各元素之和為 1,而壓縮後的值便可以作為置信率,所以常用於分類問題。另外,在實際運算的時候,為了避免上溢和下溢,在將向量丟進softmax之前往往先對每個元素減去其中最大值,即:
想了解更多,可以參考:憶臻:softmax函式計算時候為什麼要減去一個最大值?
再談談一個容易搞混的東西:
上面我們丟入一個長度為
的
向量,得到
,而softmax loss呢,則是:
其中
是一個長度為
的one-hot向量,即
,只有ground truth對應的
。所以也可以簡寫為:
到這裡我們不妨在看看交叉熵
:
其中
是真實分佈,在分類任務中,
實際上等價於上面的
。而
則是預測分佈,在分類任務中
實際上等價於上面的
。這樣一來進行化簡就得到:
我咋覺得這麼眼熟呢。。。
所以,我們可以得到:
參考連結:
https://
blog。csdn。net/u01438016
5/article/details/77284921
二、SphereFace
論文地址:
https://
arxiv。org/pdf/1704。0806
3。pdf
要想增強
的分類能力,其實就是要在分佈上做到兩點:
讓同類之間距離更近
讓不同類之間距離更遠
不妨繼續看看
:
其中
代表兩個向量
之間的夾角,如果對
歸一化,將偏置
置為0,即
,則有:
下標
表示
。
對於
我們乘上一個大於等於1的整數
:
這樣不僅放大了類之間的距離,也因放大了同類
與
之間的間隔而使類內更聚攏。
不過上述公式仍有問題:原來的
,如今
超出了向量之間的夾角函式
定義域範圍
咋辦?
那就變個函式唄,把n個cos懟起來變成一個遞減的連續的函式:
這樣一來:
如此我們就得到了SphereFace的損失函式
原論文則是:
其中
表示第
個樣本,
表示第
個樣本的
標籤,
表示第
和樣本
之間的夾角。
論文中的視覺化圖片:
Pytorch程式碼實現:
# SphereFace
class
SphereProduct
(
nn
。
Module
):
r
“”“Implement of large margin cosine distance: :
Args:
in_features: size of each input sample
out_features: size of each output sample
m: margin
cos(m*theta)
”“”
def
__init__
(
self
,
in_features
,
out_features
,
m
=
4
):
super
(
SphereProduct
,
self
)
。
__init__
()
self
。
in_features
=
in_features
self
。
out_features
=
out_features
self
。
m
=
m
self
。
base
=
1000。0
self
。
gamma
=
0。12
self
。
power
=
1
self
。
LambdaMin
=
5。0
self
。
iter
=
0
self
。
weight
=
Parameter
(
torch
。
FloatTensor
(
out_features
,
in_features
))
nn
。
init
。
xavier_uniform
(
self
。
weight
)
# duplication formula
# 將x\in[-1,1]範圍的重複index次對映到y\[-1,1]上
self
。
mlambda
=
[
lambda
x
:
x
**
0
,
lambda
x
:
x
**
1
,
lambda
x
:
2
*
x
**
2
-
1
,
lambda
x
:
4
*
x
**
3
-
3
*
x
,
lambda
x
:
8
*
x
**
4
-
8
*
x
**
2
+
1
,
lambda
x
:
16
*
x
**
5
-
20
*
x
**
3
+
5
*
x
]
“”“
執行以下程式碼直觀瞭解mlambda
import matplotlib。pyplot as plt
mlambda = [
lambda x: x ** 0,
lambda x: x ** 1,
lambda x: 2 * x ** 2 - 1,
lambda x: 4 * x ** 3 - 3 * x,
lambda x: 8 * x ** 4 - 8 * x ** 2 + 1,
lambda x: 16 * x ** 5 - 20 * x ** 3 + 5 * x
]
x = [0。01 * i for i in range(-100, 101)]
print(x)
for f in mlambda:
plt。plot(x,[f(i) for i in x])
plt。show()
”“”
def
forward
(
self
,
input
,
label
):
# lambda = max(lambda_min,base*(1+gamma*iteration)^(-power))
self
。
iter
+=
1
self
。
lamb
=
max
(
self
。
LambdaMin
,
self
。
base
*
(
1
+
self
。
gamma
*
self
。
iter
)
**
(
-
1
*
self
。
power
))
# ——————————————- cos(theta) & phi(theta) ——————————————-
cos_theta
=
F
。
linear
(
F
。
normalize
(
input
),
F
。
normalize
(
self
。
weight
))
cos_theta
=
cos_theta
。
clamp
(
-
1
,
1
)
cos_m_theta
=
self
。
mlambda
[
self
。
m
](
cos_theta
)
theta
=
cos_theta
。
data
。
acos
()
k
=
(
self
。
m
*
theta
/
3。14159265
)
。
floor
()
phi_theta
=
((
-
1。0
)
**
k
)
*
cos_m_theta
-
2
*
k
NormOfFeature
=
torch
。
norm
(
input
,
2
,
1
)
# ——————————————- convert label to one-hot ——————————————-
one_hot
=
torch
。
zeros
(
cos_theta
。
size
())
one_hot
=
one_hot
。
cuda
()
if
cos_theta
。
is_cuda
else
one_hot
one_hot
。
scatter_
(
1
,
label
。
view
(
-
1
,
1
),
1
)
# ——————————————- Calculate output ——————————————-
output
=
(
one_hot
*
(
phi_theta
-
cos_theta
)
/
(
1
+
self
。
lamb
))
+
cos_theta
output
*=
NormOfFeature
。
view
(
-
1
,
1
)
return
output
def
__repr__
(
self
):
return
self
。
__class__
。
__name__
+
‘(’
\
+
‘in_features=’
+
str
(
self
。
in_features
)
\
+
‘, out_features=’
+
str
(
self
。
out_features
)
\
+
‘, m=’
+
str
(
self
。
m
)
+
‘)’
三、CosFace
論文地址:
https://
arxiv。org/pdf/1801。0941
4。pdf
和SphereFace類似,CosFace也是從
的餘弦表達形式入手,令
。與此同時,作者發現
對於分類並沒有啥幫助,所以乾脆將其固定
,所以有:
應該代表歸一化的
。
接下來與上文
類似的是也引入了常數
,不同的是這裡的
是加上去的:
以上我們就得到了CosFace中提出的
程式碼實現:
# CosFace
class
AddMarginProduct
(
nn
。
Module
):
r
“”“Implement of large margin cosine distance: :
Args:
in_features: size of each input sample
out_features: size of each output sample
s: norm of input feature
m: margin
cos(theta) - m
”“”
def
__init__
(
self
,
in_features
,
out_features
,
s
=
30。0
,
m
=
0。40
):
super
(
AddMarginProduct
,
self
)
。
__init__
()
self
。
in_features
=
in_features
self
。
out_features
=
out_features
self
。
s
=
s
self
。
m
=
m
self
。
weight
=
Parameter
(
torch
。
FloatTensor
(
out_features
,
in_features
))
nn
。
init
。
xavier_uniform_
(
self
。
weight
)
def
forward
(
self
,
input
,
label
):
# ——————————————- cos(theta) & phi(theta) ——————————————-
cosine
=
F
。
linear
(
F
。
normalize
(
input
),
F
。
normalize
(
self
。
weight
))
phi
=
cosine
-
self
。
m
# ——————————————- convert label to one-hot ——————————————-
one_hot
=
torch
。
zeros
(
cosine
。
size
(),
device
=
‘cuda’
)
# one_hot = one_hot。cuda() if cosine。is_cuda else one_hot
one_hot
。
scatter_
(
1
,
label
。
view
(
-
1
,
1
)
。
long
(),
1
)
# ——————-torch。where(out_i = {x_i if condition_i else y_i) ——————-
output
=
(
one_hot
*
phi
)
+
((
1。0
-
one_hot
)
*
cosine
)
# you can use torch。where if your torch。__version__ is 0。4
output
*=
self
。
s
# print(output)
return
output
def
__repr__
(
self
):
return
self
。
__class__
。
__name__
+
‘(’
\
+
‘in_features=’
+
str
(
self
。
in_features
)
\
+
‘, out_features=’
+
str
(
self
。
out_features
)
\
+
‘, s=’
+
str
(
self
。
s
)
\
+
‘, m=’
+
str
(
self
。
m
)
+
‘)’
四、ArcFace
論文地址:
https://
arxiv。org/pdf/1801。0769
8。pdf
話不多說,直接上公式:
可以看到和CosFace非常類似,只是將
作為角度加上去了,這樣就強行拉大了同類之間的角度,使得神經網路更努力地將同類收得更緊。
虛擬碼實現步驟:
對
進行歸一化
對
進行歸一化
計算
得到預測向量
從
中挑出與ground truth對應的值
計算其反餘弦得到角度
角度加上m
得到挑出從
中挑出與ground truth對應的值所在位置的獨熱碼
將
透過獨熱碼放回原來的位置
對所有值乘上固定值
程式碼實現:
# ArcFace
class
ArcMarginProduct
(
nn
。
Module
):
r
“”“Implement of large margin arc distance: :
Args:
in_features: size of each input sample
out_features: size of each output sample
s: norm of input feature
m: margin
cos(theta + m)
”“”
def
__init__
(
self
,
in_features
,
out_features
,
s
=
30。0
,
m
=
0。50
,
easy_margin
=
False
):
super
(
ArcMarginProduct
,
self
)
。
__init__
()
self
。
in_features
=
in_features
self
。
out_features
=
out_features
self
。
s
=
s
self
。
m
=
m
# Parameter 的用途:
# 將一個不可訓練的型別Tensor轉換成可以訓練的型別parameter
# 並將這個parameter繫結到這個module裡面
# net。parameter()中就有這個繫結的parameter,所以在引數最佳化的時候可以進行最佳化的
# https://www。jianshu。com/p/d8b77cc02410
# 初始化權重
self
。
weight
=
Parameter
(
torch
。
FloatTensor
(
out_features
,
in_features
))
nn
。
init
。
xavier_uniform_
(
self
。
weight
)
self
。
easy_margin
=
easy_margin
self
。
cos_m
=
math
。
cos
(
m
)
self
。
sin_m
=
math
。
sin
(
m
)
self
。
th
=
math
。
cos
(
math
。
pi
-
m
)
self
。
mm
=
math
。
sin
(
math
。
pi
-
m
)
*
m
def
forward
(
self
,
input
,
label
):
# ——————————————- cos(theta) & phi(theta) ——————————————-
# torch。nn。functional。linear(input, weight, bias=None)
# y=x*W^T+b
cosine
=
F
。
linear
(
F
。
normalize
(
input
),
F
。
normalize
(
self
。
weight
))
sine
=
torch
。
sqrt
(
1。0
-
torch
。
pow
(
cosine
,
2
))
# cos(a+b)=cos(a)*cos(b)-size(a)*sin(b)
phi
=
cosine
*
self
。
cos_m
-
sine
*
self
。
sin_m
if
self
。
easy_margin
:
# torch。where(condition, x, y) → Tensor
# condition (ByteTensor) – When True (nonzero), yield x, otherwise yield y
# x (Tensor) – values selected at indices where condition is True
# y (Tensor) – values selected at indices where condition is False
# return:
# A tensor of shape equal to the broadcasted shape of condition, x, y
# cosine>0 means two class is similar, thus use the phi which make it
phi
=
torch
。
where
(
cosine
>
0
,
phi
,
cosine
)
else
:
phi
=
torch
。
where
(
cosine
>
self
。
th
,
phi
,
cosine
-
self
。
mm
)
# ——————————————- convert label to one-hot ——————————————-
# one_hot = torch。zeros(cosine。size(), requires_grad=True, device=‘cuda’)
# 將cos(\theta + m)更新到tensor相應的位置中
one_hot
=
torch
。
zeros
(
cosine
。
size
(),
device
=
‘cuda’
)
# scatter_(dim, index, src)
one_hot
。
scatter_
(
1
,
label
。
view
(
-
1
,
1
)
。
long
(),
1
)
# ——————-torch。where(out_i = {x_i if condition_i else y_i) ——————-
output
=
(
one_hot
*
phi
)
+
((
1。0
-
one_hot
)
*
cosine
)
# you can use torch。where if your torch。__version__ is 0。4
output
*=
self
。
s
# print(output)
return
output
到此ArcFace、SphereFace、CosFace的損失函式就介紹完啦~
參考連結:
https://
blog。csdn。net/fuwenyan/
article/details/79657738
歡迎關注個人微信公眾號:
歡迎掃碼關注~