Face Recognition Loss on Mnist with Pytorch

寫在前面

這篇文章的重點不在於講解FR的各種Loss,因為知乎上已經有很多,搜一下就好,本文主要提供了各種Loss的Pytorch實現以及Mnist的可視化實驗,一方面讓大家藉助代碼更深刻地理解Loss的設計,另一方面直觀的比較各種Loss的有效性,是否漲點並不是我關注的重點,因為這些Loss的設計理念之一就是增大收斂難度,所以在Mnist這樣的簡單任務上訓練同樣的epoch,先進的Loss並不一定能帶來點數的提升,但從視覺效果可以明顯的看出特徵的分離程度,而且從另一方面來說,分類正確不代表一定能能在用歐式/餘弦距離做1:1驗證的時候也正確...

本文主要仿照CenterLoss文中的實驗結構,使用了一個相對複雜一些的LeNet升級版網路,把輸入圖片Embedding成2維特徵向量以便於可視化。

對了,代碼里用到了TensorBoardX來可視化,當然如果你沒裝,可以注釋掉相關代碼,我也寫了本地保存圖片,雖然很不喜歡TensorFlow,但TensorBoard還是真香,比Visdom強太多了...

早就想寫這篇文章了,趁著五一假期終於...

具體代碼在Github:github.com/MccreeZhao/F 有興趣的話點個Star呀~雖然剛起步還沒什麼東西

文章里只展示loss寫法

Softmax

公式推導

 L_1 = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{W_{yi}^T x_i+b_{y_i}}} {sum_{j=1}^n e^{W_j^Tx_i + b_j} } = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{||W_{yi}||cdot|| x_i||cos(	heta_{y_i})+b_{y_i}}} {sum_{j=1}^n e^{||W_j||cdot||x_i||cos(	heta_{j}) + b_j} }

Pytorch代碼實現

class Linear(nn.Module):
def __init__(self):
super(Linear, self).__init__()
self.weight = nn.Parameter(torch.Tensor(2,10))#(input,output)
self.bias = nn.Parameter(torch.Tensor(1,10))
nn.init.xavier_uniform_(self.weight)

def forward(self, x):
out = x.mm(self.weight)+self.bias
return out
criterion = nn.CrossEntropyLoss()
loss = criterion(out,label)
#CrossEntropyLoss等同於nn.LogSoftmax()+nn.NLLLoss()

emmm...現實生活中根本沒人會這麼寫好吧!明明就有現成的Linear層啊喂!

寫成這樣只是為了方便統一框架...

可視化

這一張圖是二維化的特徵,注意觀察不同兩類任意點之間的餘弦距離和歐氏距離

這張圖是將特徵歸一化的結果,能更好的反映餘弦距離,豎線是該類在最後一個FC層的權重,等同於類別中心(這一點對於理解loss的發展還是挺關鍵的)

後面的圖片也都是這種形式,大家可以比較著來看

Modified Softmax

公式推導

 L_2 = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{||x_i||cdot cos(	heta_{y_i})}} {sum_{j=1}^n e^{||x_i||cdot cos(	heta_{j})} }

去除了權重的模長和偏置對loss的影響,將特徵映射到了超球面,同時避免了樣本量差異帶來的預測傾向性(樣本量大可能導致權重模長偏大)

Pytorch代碼實現

class Modified(nn.Module):
def __init__(self):
super(Modified, self).__init__()
self.weight = nn.Parameter(torch.Tensor(2,10))#(input,output)
nn.init.xavier_uniform_(self.weight)
self.weight.data.uniform_(-1,1).renorm_(2,1,1e-5).mul_(1e5)
#因為renorm採用的是maxnorm,所以先縮小再放大以防止norm結果小於1

def forward(self, x):
w=self.weight
ww=w.renorm(2,1,1e-5).mul(1e5)
out = x.mm(ww)
return out

可視化

這裡要提一句,如果大家留心的話可以發現,雖然modified loss並沒有太好的聚攏效果,但確讓類別中心準確地落在了feature的中心,這對於網路的性能是有很大好處的,但是具體原因我沒想出來...希望能有大佬在評論區給解釋一下...

NormFace

既然權重的模長有影響,Feature的模長必然也有影響,具體還是看文章,另外,質量差的圖片feature模長往往較短,做normalize之後消除了這個影響,有利有弊,還沒有達成一致觀點,目前主流的Loss還是包括feature normalize的

公式推導

 L_3 = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{scdot cos(	heta_{y_i})}} {sum_{j=1}^n e^{ scdot cos(	heta_{j})} }

可視化

就是一個字:猛! 感覺有了NormFace,後面的花式Loss都體現不出來效果了...

Pytorch代碼實現

class NormFace(nn.Module):
def __init__(self):
super(NormFace, self).__init__()
self.weight = nn.Parameter(torch.Tensor(2,10))#(input,output)
nn.init.xavier_uniform_(self.weight)
self.weight.data.uniform_(-1,1).renorm_(2,1,1e-5).mul_(1e5)
#因為renorm採用的是maxnorm,所以先縮小再放大以防止norm結果小於1

def forward(self, x, epoch):
x=x.renorm(2,0,1e-5).mul(1e5)
w=self.weight
ww=w.renorm(2,1,1e-5).mul(1e5)
out = x.mm(ww)#*64
return out

SphereFace:A-softmax

為了進一步約束特徵向量之間的餘弦距離,我們人為地增加收斂難度,給兩個向量之間的夾角乘上一個因子:m

公式推導

L_4 = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{||x_i||cdot cos(mcdot	heta_{y_i})}} {e^{||x_i||cdot cos(mcdot	heta_{y_i})}+sum_{j
eq y_i}^n e^{||x_i||cdot cos(	heta_{j})} }

Pytorch代碼實現

class AngleLinear(nn.Module):
def __init__(self, m=4):
super(AngleLinear, self).__init__()
self.weight = nn.Parameter(torch.Tensor(2, 10)) # (input,output)
nn.init.xavier_uniform_(self.weight)
self.weight.data.renorm_(2, 1, 1e-5).mul_(1e5)
# 因為renorm採用的是maxnorm,所以先縮小再放大以防止norm結果小於1
self.m = m
self.mlambda = [ # 求cos(mx)的公式
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
]

def forward(self, input, epoch):
#注意,在原始的A-softmax中是不對x進行標準化的,
#標準化可以提升性能,也會增加收斂難度,A-softmax本來就很難收斂
#可能因為難收斂所以才沒有在人臉中使用,但是mnist是能夠收斂的
x = input#.renorm(2,0,1e-5).mul(1e5) # (Batch_size,F) F=len(feature),here is 2
w = self.weight # (F,Classnum)
ww = w.renorm(2, 1, 1e-5).mul(1e5)
x_norm = x.pow(2).sum(1).pow(0.5)
cos_theta = x.mm(ww) / x_norm.view(-1, 1)
cos_theta = cos_theta.clamp(-1, 1) # 防止出現異常
# 以上計算出了傳統意義上的cos_theta,但為了cos(m*theta)的單調遞減,需要使用phi_theta

cos_m_theta = self.mlambda[self.m](cos_theta)
# 計算theta,依據theta的區間把k的取值定下來
theta = cos_theta.data.acos()
k = (self.m * theta / 3.1415926).floor()
phi_theta = ((-1) ** k) * cos_m_theta - 2 * k

x_cos_theta = cos_theta * x_norm.view(-1, 1)
x_phi_theta = phi_theta * x_norm.view(-1, 1)
output = (x_cos_theta,x_phi_theta)
#注意,只有標籤對應項需要替換為cos(m*theta),所以兩個值都需要傳遞給損失函數
return output

class AngleLoss(nn.Module):
def __init__(self,gamma = 0):
super(AngleLoss,self).__init__()
self.gamma = gamma
self.it = 0
self.LambdaMin = 3
self.LambdaMax = 30000.0
self.lamb = 30000.0#lambda和正則化表達式的關鍵字重名了
def forward(self, input,target):
self.it += 1#用來調整lambda
x_cos_theta,x_phi_theta = input
target = target.view(-1,1) #(B,1)

onehot = torch.zeros(target.shape[0], 10).cuda().scatter_(1, target, 1)

self.lamb = max(self.LambdaMin,self.LambdaMax/(1+0.2*self.it))

output = x_cos_theta*1.0#如果不乘可能會有數值錯誤?
output[onehot.byte()] -= x_cos_theta[onehot.byte()]*(1.0+0)/(1+self.lamb)
#不能直接置零,因為是按比例分配cos和phi的
output[onehot.byte()] += x_phi_theta[onehot.byte()]*(1.0+0)/(1+self.lamb)
#到這一步可以等同於原來的Wx+b=y的輸出了,

#到這裡使用了Focal Loss,如果直接使用cross_Entropy的話似乎效果會減弱許多
log = F.log_softmax(output)
log = log.gather(1,target)

log = log.view(-1)
pt = log.data.exp()
loss = -1*(1-pt)**self.gamma*log

loss = loss.mean()
#loss = F.cross_entropy(x_cos_theta,target.view(-1))
return loss,output

可視化

InsightFace(ArcSoftmax)

公式推導

L_5 = -frac{1}{m}sum_{i=1}^{m}logfrac{e^{scdot cos(	heta_{y_i}+m)}} {e^{scdot cos(	heta_{y_i}+m)}+sum_{j
eq y_i}^n e^{scdot cos(	heta_{j})} }

Pytorch代碼實現

class ArcMarginProduct(nn.Module):
def __init__(self,s=256, m=0.01):
super(ArcMarginProduct, self).__init__()
self.in_feature = 2
self.out_feature = 10
self.s = s
self.m = m
self.weight = nn.Parameter(torch.Tensor(2, 10)) # (input,output)
nn.init.xavier_uniform_(self.weight)
self.weight.data.renorm_(2, 1, 1e-5).mul_(1e5)

self.cos_m = math.cos(m)
self.sin_m = math.sin(m)
# 為了保證cos(theta+m)在0-pi單調遞減:
self.th = math.cos(3.1415926 - m)
self.mm = math.sin(3.1415926 - m) * m

def forward(self, x, label):
cosine = F.normalize(x).mm(F.normalize(self.weight, dim=0))
sine = torch.sqrt(1.0 - torch.pow(cosine, 2))
phi = cosine * self.cos_m - sine * self.sin_m # 兩角和公式
# 為了保證cos(theta+m)在0-pi單調遞減:
phi = torch.where((cosine - self.th) > 0, phi, cosine - self.mm)
one_hot = torch.zeros_like(cosine)
one_hot.scatter_(1, label.view(-1, 1), 1)
output = (one_hot * phi) + ((1.0 - one_hot) * cosine)
output = output * self.s
#這裡這個s比較關鍵,如果設置的不夠大會導致收斂困難,
# 比較容易出現兩個類別的Feature重疊的現象,
# 這也是加性Margin相對於乘性Margin的一個比較弱勢的地方,
# 對於特徵向量的收縮要求足夠,但是對兩類特徵向量之間的距離約束不夠
loss = F.cross_entropy(cosine, label)

return loss, output

可視化

ArcSoftmax需要更久的訓練,這個收斂還不夠充分...顏值堪憂,另外ArcSoftmax經常出現類別在特徵空間分布不均勻的情況,這個也有點費解,難道在訓FR模型的時候先用softmax然後慢慢加margin有奇效?SphereFace那種退火的訓練方式效果好會不會和這個有關呢...

Center Loss

亂入一個歐式距離的細作

公式推導

 L_6 = --frac{1}{m}sum_{i=1}^{m}logfrac{e^{||x_i||cdot cos(	heta_{y_i})}} {sum_{j=1}^n e^{||x_i||cdot cos(	heta_{j})} } + frac{lambda}{2}sum_{i=1}^m||x_i-c_{y_i}||^2

其中 c_{y_i} 是每個類別對應的一個中心,在這裡就是一個二維坐標啦

Pytorch代碼實現

class centerloss(nn.Module):
def __init__(self,num_classes=10,feat_dim=2):
super(centerloss,self).__init__()
self.center = nn.Parameter(10*torch.randn(10,2).cuda())
self.num_classes = num_classes
self.feat_dim = feat_dim
def forward(self,feature,label):
batch_size = label.size()[0]
nCenter = self.center.index_select(dim=0,index=label)
distance = feature.dist(nCenter)
loss = (1/2.0/batch_size)*distance
return loss

這裡實現的是center的部分,還要跟原始的CEloss相加的,具體看github吧

可視化

會不會配合weight norm效果更佳呢?以後再說吧...

總結

先寫到這裡,如果大家有興趣可以去github點個star之類的...作為一個研一快結束的弱雞剛剛學會使用github...也是沒誰了...

參考文獻:

Wang M, Deng W. Deep face recognition: A survey[J]. arXiv preprint arXiv:1804.06655, 2018.

覺得自己真是個小機靈鬼...(逃)


推薦閱讀:

TAG:深度學習(DeepLearning) | 人臉識別 | PyTorch |