深度学习入门(十二)一周精读论文与代码复刻 ArcFace (Day 2)

Dec 16, 2018


我制作的工程代码在这:Tiny_Face_Recognition

论文地址:https://arxiv.org/abs/1801.07698

参考代码地址(使用的MXNet):https://github.com/deepinsight/insightface#512-d-feature-embedding

训练集:MS-Celeb-1M

验证集:LFW

注:我的公式里的i和j,与原论文中的i和j正好反了过来,为的是方便我自己理解和编辑公式

回顾与问题

A-Softmax Loss继续

在昨天,主要介绍了一下SphereFace的写法,公式如下:

其中$\psi{(\theta_{y_j})} = (-1)^k \cos{(m\theta_{y_j})} - 2k, \theta_{y_j}\in [\frac{k\pi}{m}, \frac{(k+1)\pi}{m}], k \in [0, m-1]$

第一个问题是:为什么要引入这个$\psi$,直接用$\cos(m\theta)$不好吗?

因为$\cos(m\theta)$在$[0, \pi]$上才是单调递减的(为了在空间上区分出来,必须使得是单调递减的),这样一来$\theta_{y_j}\in [0, \frac{\pi}{m}]$,这个约束应该算是比较小的,为了能够让网络学到更多的角度,所以要消除掉这个约束,让其一直随着角度单调递减即可,这样就有了上面的这个$\psi$函数。

在SphereFace的论文中,对这个$\psi$又进行了更改,引入了一个$\lambda$参数去控制:

同时用$s=64$作为feature scale(特征归一化参数?)替代了$|x|$

最终得到的公式为:

其中$s=64$,然后$\psi$就是之后修改的带有控制参数的。

Cosine Margin Loss

注意到A-Softmax Loss中的主体是$\cos(m\theta_{y_j})$,首先把之前做的$\psi$去掉,变回cos,再将其修改为$\cos(\theta_{y_j}) - m$即可,也就是把m从cos里面提出来,和cos做一个减法。

为什么要这么做?

因为去掉了$\psi$,算起来更简单,训练过程更简洁快速。具体效果我也不清楚,因为没有试过,不过他说啥就是啥吧…

Angular Margin Loss (ArcFace)

首先我们要分别对$W和$x$进行L2正则后替换,即:

计算正弦值:

然后整个ArcFace Loss公式为:

首先,直观上来看,是不是要简洁了很多,那还是老问题,为什么要这么做?

原论文中只是说效果比较好,我看了网上的说法,说$\cos(\theta + m) = \cos\theta \cos m - \sin\theta \sin m$,相比于Cosine Margin Loss中的$\cos\theta -m$,不仅动态依赖$\sin\theta$,还能使得网络学到更多的角度特性。

评测和比较

为了方便比较这一堆Loss,作者弄了一个结合的公式,引人了三个m,通过控制这三个m的值让其变为不同的Loss,即:

比如:

m1=1, m2=0, m3=0 即为传统Softmax

m1=1, m2=0, m3!=0, 即为CosFace

具体的比较和三个参数的值可以看下图:

image

右图表示当m取三个不同的值,化身为几种不同的loss函数的收敛情况。

左图则表示使用ArcFace Loss训练,训练开始、训练到中间、训练结束的三段时间中,$\theta$的分布情况,差不多结束的时候都会偏向左边,也就是差不多30度前后20度的样子。

对比其他种类的Loss

然后到现在他算是把这些关系都说清楚了,接下来论文简单说了一下和Intra-Loss, Inter-Loss,以及Triplet Loss的情况。为了达到更好的实际效果,所以我们可以把我们的这一堆Softmax系列的Loss结合这些其他的Loss。

想像一个空间,比如一个球,你训练两张照片,两张经过计算都会投射到不同的空间,如果是一个人的,我们希望两个投射的空间能越接近越好,如果是不同人的,我们需要它们离得越远越好。

Intra-Loss,Intra是内部的意思,很明显这是一类内部的Loss,我们希望能更紧凑一些,来自同一个人的脸,能够映射到紧凑的空间里,意思很简单,减少样本和真值间差的角度:

Inter-Loss,Inter是之间的意思,也就是不同类别之间要加大间隙,尽量远离,所以增大不同类别的中心的距离:

作者提到,这种Inter-Loss是一种叫做Minimum Hyper-spherical Energy(MHE)的方法中的一个特殊情况。

Triplet Loss,原始的TL是做的Euclidean margin,在这里直接转化为角度,即外面套个arccos:

实验

到这里,作者介绍的Loss相关的理论就到此为止了,下面就是实验进行评测。

数据集介绍先不用管,我们来看一下他的实验配置,由于这里非常的重要,所以这两段要精细的逐句进行说明。

Experimental Settings

For data prepossessing(他这个词写错了吧..preprocessing?), we follow the recent papers[17, 35] to generate the normalised face crops(112 * 112) by utilising five facial points.

数据预处理,我们按照两篇论文使用脸部五点的方法得到了112*112尺寸的人脸图片。

两篇论文分别是: 1. Sphereface: Deep hypersphere embedding for face recognition. In CVPR 2017,即上篇中引入Sphereface的论文

  1. CosFace: Large margin cosine loss for deep face recognition. In CVPR 2018,这篇应该是引入Cosine Margin的那篇论文,可以看到非常的新,正是今年CVPR中的论文

For the embedding network, we employ the widely used CNN architectures, ResNet50 and ResNet100,[11, 10].

特征提取的网络,我们用了两篇论文中的ResNet50和100。

两篇论文分别是:1. Deep pyramidal residual networks. arXiv:1610.02915,2016

  1. Deep residual learning for image recognition. In CVPR,2016

After the last convolutional layer, we exploe the BN[13]-Dropout[29]-FC-BN structure to get the final 512-D embedding feature. In this paper, we use ([training dataset, network structure, loss]) to facilitate understanding of the experimental settings.

在最后一层卷积层后,我们加了一个BN-Dropout-FC-BN的结构(又是两篇论文,这俩是基本概念,就不列了,不懂的话可以搜一下Batch Normalization和Dropout)

We follow 35 to set the feature scale s to 64 and choose the angular margin m of ArcFace at 0.5.

设置两个参数,s=64, m=0.5

All experiments in this paper are implemented by MXNet[7]. We set the batch size to 512 and train models on four NVIDIA Tesla P40(24GB) GPUs.

他的实验环境是MXNet, Batch size 512, 四块P40显卡上跑。

而我当然是TensorFlow, 一块P40…

On CASIA, the learning rate starts from 0.1 and is divided by 10 at 20K, 28K iterations. The training process is finished at 32K iterations.

CASIA数据集上,lr从0.1开始,20K和28K迭代次数,最后在32K迭代结束。

on MS1MV2, we divide the learning rate at 100K, 160K iterations and finish at 180K iterations.

MS1MV2(我要用的数据集)上,在180K迭代结束。

We set momentum to 0.9 and weight decay to 5e-4. During testing, we only keep the feature embedding network without the fully connected layer(160MB for ResNet50 and 250MB for ResNet100) and extract the 512-D features(8.9 ms/face for ResNet50 and 15.4 for ResNet100) for each normalised face.

参数momentum 设置为0.9,weight decay为5e-4。

测试集的时候,我们不经过全连接层,然后提取512维特征,速度在括号里。

后面是视频数据集的处理,这里略。

第二天代码实现

昨天搭好了一个简易的能用的ResNet50结构,今天论文学习中引入了作者的实验配置,那么我也要先把自己环境的配置设好。

参考我在学习声纹识别时看到的别人使用argparse引入参数的方法,很方便。

概要设计与参数如下:

import argparse

def main(args):
	# TODO
	# preprocessing() 预处理,包含分割训练测试,batch的循环制作
	# placeholder  变量x和y,及类别数class等等
	# network      得到网络结构,输入x,得到embeddings,ResNet50的部分参数待修改
	# loss         输入embeddings和标签值y,代入ArcFace计算Loss
	# optimizer    原文没说(可能我没看到...)
	# step         用optimizer优化loss
	# accuracy     准确率计算

	# session      开启会话,GPU声明与其他配置声明,参数保存Saver声明等
	# iterations的循环  
	#     得到x和y的batch,输入到sess.run(),图计算更新参数
	#     %display_step 计算准确率,保存当前参数
	pass

# 参数们,还会继续增加
def parse_arguments(argv):
	parser = argparse.ArgumentParser()
	parser.add_argument('--pretrained_model', type=str, help='pretrained model path.', default=None)
	parser.add_argument('--max_step', type=str, help='Numbers of steps to run.', default=180000)
	parser.add_argument('--batch_size', type=int, help='Batch size.', default=512)
	parser.add_argument('--CUDA_VISIBLE_DEVICES', type=str, help='CUDA VISIBLE DEVICES', default='9')
	parser.add_argument('--models_base_dir', type=str, help='Directory where to write trained models and checkpoints.', default='./model_ckpt')
	parser.add_argument('--finetuning', type=bool, help='Whether finetuning.', default=False)
	parser.add_argument('--data_set', type=str, help='Data set path', default='/data/ChuyuanXiong/extract')
	parser.add_argument('--keep_prob', type=float, help='keep probability.', default=0.5)
	parser.add_argument('--embedding_size', type=int, help='Embedding size.', default=512)
	parser.add_argument('--momentum', type=float, help='momentum', default=0.9)
	parser.add_argument('--decay', type=float, help='Weight decay', default=5e-4)
	parser.add_argument('--arcface_s', type=int, help='s', default=64)
	parser.add_argument('--arcface_m', type=float, help='margin', default=0.5)


if __name__ == '__main__':
	main(parse_arguments(sys.argv[1:]))