• (原)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 olimits_{j}{{{e}^{W_{j}^{T}{{x}_{i}}+{{b}_{j}}}}}}=-log frac{{{e}^{left| {{W}_{yi}} ight|left| {{x}_{i}} ight|cos ({{ heta }_{yi}},i)+{{b}_{yi}}}}}{sum olimits_{j}{{{e}^{left| {{W}_{j}} ight|left| {{x}_{i}} ight|cos ({{ heta }_{j}},i)+{{b}_{j}}}}}}$

    将W归一化到1,且不考虑偏置项,即${{b}_{j}}=0$,则上式变成:

    ${{L}_{ ext{modified}}}=frac{1}{N}sumlimits_{i}{-log (frac{{{e}^{left| {{x}_{i}} ight|cos ({{ heta }_{yi}},i)}}}{sum olimits_{j}{{{e}^{left| {{x}_{i}} ight|cos ({{ heta }_{j}},i)}}}}})$

    其中θ为w和x的夹角。

    为了进一步限制夹角的范围,使用mθ,上式变成

    ${{L}_{ ext{ang}}}=frac{1}{N}sumlimits_{i}{-log (frac{{{e}^{left| {{x}_{i}} ight|cos (m{{ heta }_{yi}},i)}}}{{{e}^{left| {{x}_{i}} ight|cos (m{{ heta }_{yi}},i)}}+sum olimits_{j e yi}{{{e}^{left| {{x}_{i}} ight|cos ({{ heta }_{j}},i)}}}}})$

    其中θ范围为$left[ 0,frac{pi }{m} ight]$。

    为了使得上式单调,引入$psi ({{ heta }_{yi,i}})$:

    ${{L}_{ ext{ang}}}=frac{1}{N}sumlimits_{i}{-log (frac{{{e}^{left| {{x}_{i}} ight|psi ({{ heta }_{yi,i}})}}}{{{e}^{left| {{x}_{i}} ight|psi ({{ heta }_{yi,i}})}}+sum olimits_{j e yi}{{{e}^{left| {{x}_{i}} ight|cos ({{ heta }_{j}},i)}}}}})$

    其中

    $psi ({{ heta }_{yi,i}})={{(-1)}^{k}}cos (m{{ heta }_{yi,i}})-2k$,${{ heta }_{yi,i}}in left[ frac{kpi }{m},frac{(k+1)pi }{m} ight]$,$kin left[ 0,m-1 ight]$,$mge 1$

    代码中引入了超参数λ,为

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

    其中,${{lambda }_{min }}=5$,${{lambda }_{max }}=1500$为程序中预先设定的值。

    实际的$psi ( heta )$为

    $psi ({{ heta }_{yi}})=frac{{{(-1)}^{k}}cos (m{{ heta }_{yi}})-2k+lambda cos ({{ heta }_{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 ({{ heta }_{yi}})-frac{cos ({{ heta }_{yi}})}{1+lambda }+frac{psi ({{ heta }_{yi}})}{1+lambda }=frac{psi ({{ heta }_{yi}})+lambda cos ({{ heta }_{yi}})}{1+lambda }=frac{{{(-1)}^{k}}cos (m{{ heta }_{yi}})-2k+lambda cos ({{ heta }_{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
  • 相关阅读:
    redis-hash
    redis-list操作
    bootstrap之消息提示
    jQuery水平下拉菜单实现
    JavaScript的Date对象
    积水问题
    Queue的push和front操作
    Stack的pop和push操作
    .py文件不能设置默认打开程序 win10
    Anaconda的安装
  • 原文地址:https://www.cnblogs.com/darkknightzh/p/8524937.html
Copyright © 2020-2023  润新知