轉載請注明出處:
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