(原)SphereFace及其pytorch代碼


轉載請注明出處:

http://www.cnblogs.com/darkknightzh/p/8524937.html

論文:

SphereFace: Deep Hypersphere Embedding for Face Recognition

https://arxiv.org/abs/1704.08063

http://wyliu.com/papers/LiuCVPR17v3.pdf

官方代碼:

https://github.com/wy1iu/sphereface

pytorch代碼:

https://github.com/clcarwin/sphereface_pytorch

 

說明:沒用過mxnet,下面的代碼注釋只是純粹從代碼的角度來分析並進行注釋,如有錯誤之處,敬請諒解,並歡迎指出。

 

傳統的交叉熵公式如下:

${{L}_{i}}=-\log \frac{{{e}^{W_{yi}^{T}{{x}_{i}}+{{b}_{yi}}}}}{\sum\nolimits_{j}{{{e}^{W_{j}^{T}{{x}_{i}}+{{b}_{j}}}}}}=-\log \frac{{{e}^{\left\| {{W}_{yi}} \right\|\left\| {{x}_{i}} \right\|\cos ({{\theta }_{yi}},i)+{{b}_{yi}}}}}{\sum\nolimits_{j}{{{e}^{\left\| {{W}_{j}} \right\|\left\| {{x}_{i}} \right\|\cos ({{\theta }_{j}},i)+{{b}_{j}}}}}}$

將W歸一化到1,且不考慮偏置項,即${{b}_{j}}=0$,則上式變成:

${{L}_{\text{modified}}}=\frac{1}{N}\sum\limits_{i}{-\log (\frac{{{e}^{\left\| {{x}_{i}} \right\|\cos ({{\theta }_{yi}},i)}}}{\sum\nolimits_{j}{{{e}^{\left\| {{x}_{i}} \right\|\cos ({{\theta }_{j}},i)}}}}})$

其中θ為w和x的夾角。

為了進一步限制夾角的范圍,使用mθ,上式變成

${{L}_{\text{ang}}}=\frac{1}{N}\sum\limits_{i}{-\log (\frac{{{e}^{\left\| {{x}_{i}} \right\|\cos (m{{\theta }_{yi}},i)}}}{{{e}^{\left\| {{x}_{i}} \right\|\cos (m{{\theta }_{yi}},i)}}+\sum\nolimits_{j\ne yi}{{{e}^{\left\| {{x}_{i}} \right\|\cos ({{\theta }_{j}},i)}}}}})$

其中θ范圍為$\left[ 0,\frac{\pi }{m} \right]$。

為了使得上式單調,引入$\psi ({{\theta }_{yi,i}})$:

${{L}_{\text{ang}}}=\frac{1}{N}\sum\limits_{i}{-\log (\frac{{{e}^{\left\| {{x}_{i}} \right\|\psi ({{\theta }_{yi,i}})}}}{{{e}^{\left\| {{x}_{i}} \right\|\psi ({{\theta }_{yi,i}})}}+\sum\nolimits_{j\ne yi}{{{e}^{\left\| {{x}_{i}} \right\|\cos ({{\theta }_{j}},i)}}}}})$

其中

$\psi ({{\theta }_{yi,i}})={{(-1)}^{k}}\cos (m{{\theta }_{yi,i}})-2k$,${{\theta }_{yi,i}}\in \left[ \frac{k\pi }{m},\frac{(k+1)\pi }{m} \right]$,$k\in \left[ 0,m-1 \right]$,$m\ge 1$

代碼中引入了超參數λ,為

$\lambda =\max ({{\lambda }_{\min }},\frac{{{\lambda }_{\max }}}{1+0.1\times iterator})$

其中,${{\lambda }_{\min }}=5$,${{\lambda }_{\max }}=1500$為程序中預先設定的值。

實際的$\psi (\theta )$為

$\psi ({{\theta }_{yi}})=\frac{{{(-1)}^{k}}\cos (m{{\theta }_{yi}})-2k+\lambda \cos ({{\theta }_{yi}})}{1+\lambda }$

對應下面代碼為:

output = cos_theta * 1.0
output[index] -= cos_theta[index]*(1.0+0)/(1+self.lamb)
output[index] += phi_theta[index]*(1.0+0)/(1+self.lamb)

對於yi處的計算,

$output(yi)=\cos ({{\theta }_{yi}})-\frac{\cos ({{\theta }_{yi}})}{1+\lambda }+\frac{\psi ({{\theta }_{yi}})}{1+\lambda }=\frac{\psi ({{\theta }_{yi}})+\lambda \cos ({{\theta }_{yi}})}{1+\lambda }=\frac{{{(-1)}^{k}}\cos (m{{\theta }_{yi}})-2k+\lambda \cos ({{\theta }_{yi}})}{1+\lambda }$

和上面的公式對應。

具體的代碼如下(完整的代碼見參考網址):

 1 class AngleLinear(nn.Module):
 2     def __init__(self, in_features, out_features, m = 4, phiflag=True):
 3         super(AngleLinear, self).__init__()
 4         self.in_features = in_features
 5         self.out_features = out_features
 6         self.weight = Parameter(torch.Tensor(in_features,out_features))
 7         self.weight.data.uniform_(-1, 1).renorm_(2,1,1e-5).mul_(1e5)
 8         self.phiflag = phiflag
 9         self.m = m
10         self.mlambda = [
11             lambda x: x**0,  # cos(0*theta)=1
12             lambda x: x**1,  # cos(1*theta)=cos(theta)
13             lambda x: 2*x**2-1, # cos(2*theta)=2*cos(theta)**2-1
14             lambda x: 4*x**3-3*x,
15             lambda x: 8*x**4-8*x**2+1,
16             lambda x: 16*x**5-20*x**3+5*x
17         ]
18 
19     def forward(self, input):  # input為輸入的特征,(B, C),B為batchsize,C為圖像的類別總數
20         x = input   # size=(B,F),F為特征長度,如512
21         w = self.weight # size=(F,C)
22 
23         ww = w.renorm(2,1,1e-5).mul(1e5) #對w進行歸一化,renorm使用L2范數對第1維度進行歸一化,將大於1e-5的截斷,乘以1e5,使得最終歸一化到1.如果1e-5設置的過大,裁剪時某些很小的值最終可能小於1。注意,第0維度只對每一行進行歸一化(每行平方和為1),第1維度指對每一列進行歸一化。由於w的每一列為x的權重,因而此處需要對每一列進行歸一化。如果要對x歸一化,需要對每一行進行歸一化,此時第二個參數應為0
24         xlen = x.pow(2).sum(1).pow(0.5) # 對輸入x求平方,而后對不同列求和,再開方,得到每行的模,最終大小為第0維的,即B(由於對x不歸一化,但是計算余弦時需要歸一化,因而可以先計算模。但是對於w,不太懂為何不直接使用這種方式,而是使用renorm函數?)
25         wlen = ww.pow(2).sum(0).pow(0.5) # 對權重w求平方,而后對不同行求和,再開方,得到每列的模(理論上之前已經歸一化,此處應該是1,但第一次運行到此處時,並不是1,不太懂),最終大小為第1維的,即C
26 
27         cos_theta = x.mm(ww) # 矩陣相乘(B,F)*(F,C)=(B,C),得到cos值,由於此處只是乘加,故未歸一化
28         cos_theta = cos_theta / xlen.view(-1,1) / wlen.view(1,-1) # 對每個cos值均除以B和C,得到歸一化后的cos值
29         cos_theta = cos_theta.clamp(-1,1) #將cos值截斷到[-1,1]之間,理論上不截斷應該也沒有問題,畢竟w和x都歸一化后,cos值不可能超出該范圍
30 
31         if self.phiflag:
32             cos_m_theta = self.mlambda[self.m](cos_theta) # 通過cos_theta計算cos_m_theta,mlambda為cos_m_theta展開的結果
33             theta = Variable(cos_theta.data.acos()) # 通過反余弦,計算角度theta,(B,C)
34             k = (self.m*theta/3.14159265).floor() # 通過公式,計算k,(B,C)。此處為了保證theta大於k*pi/m,轉換過來就是m*theta/pi,再向上取整
35             n_one = k*0.0 - 1 # 通過k的大小,得到同樣大小的-1矩陣,(B,C)
36             phi_theta = (n_one**k) * cos_m_theta - 2*k # 通過論文中公式,得到phi_theta。(B,C)
37         else:
38             theta = cos_theta.acos() # 得到角度theta,(B, C),每一行為當前特征和w的每一列的夾角
39             phi_theta = myphi(theta,self.m) # 
40             phi_theta = phi_theta.clamp(-1*self.m,1)
41 
42         cos_theta = cos_theta * xlen.view(-1,1)  # 由於實際上不對x進行歸一化,此處cos_theta需要乘以B。(B,C)
43         phi_theta = phi_theta * xlen.view(-1,1)  # 由於實際上不對x進行歸一化,此處phi_theta需要乘以B。(B,C)
44         output = (cos_theta,phi_theta)
45         return output # size=(B,C,2)
46 
47 
48 class AngleLoss(nn.Module):
49     def __init__(self, gamma=0):
50         super(AngleLoss, self).__init__()
51         self.gamma   = gamma
52         self.it = 0
53         self.LambdaMin = 5.0
54         self.LambdaMax = 1500.0
55         self.lamb = 1500.0
56 
57     def forward(self, input, target):
58         self.it += 1
59         cos_theta,phi_theta = input # cos_theta,(B,C)。 phi_theta,(B,C)
60         target = target.view(-1,1) #size=(B,1)
61 
62         index = cos_theta.data * 0.0 #得到和cos_theta相同大小的全0矩陣。(B,C)
63         index.scatter_(1,target.data.view(-1,1),1) # 得到一個one-hot矩陣,第i行只有target[i]的值為1,其他均為0
64         index = index.byte() # index為float的,轉換成byte類型
65         index = Variable(index)
66 
67         self.lamb = max(self.LambdaMin,self.LambdaMax/(1+0.1*self.it))  # 得到lamb
68         output = cos_theta * 1.0 #size=(B,C)  # 如果直接使用output=cos_theta,可能不收斂(未測試,但其他程序中碰到過直接對輸入使用[index]無法收斂,加上*1.0可以收斂的情況)
69         output[index] -= cos_theta[index]*(1.0+0)/(1+self.lamb) # 此行及下一行將target[i]的值通過公式得到最終輸出
70         output[index] += phi_theta[index]*(1.0+0)/(1+self.lamb)
71 
72         logpt = F.log_softmax(output) # 得到概率
73         logpt = logpt.gather(1,target) # 下面為交叉熵的計算(和focal loss的計算有點類似,當gamma為0時,為交叉熵)。
74         logpt = logpt.view(-1)
75         pt = Variable(logpt.data.exp())
76 
77         loss = -1 * (1-pt)**self.gamma * logpt
78         loss = loss.mean()
79         
80         # target = target.view(-1)  # 若要簡化,理論上可直接使用這兩行計算交叉熵(此處未測試,在其他程序中使用后可以正常訓練)
81         # loss = F.cross_entropy(cos_theta, target)
82 
83         return loss

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM