深度学习入门(十三)一周精读论文与代码复刻 ArcFace网络结构与源码分析 (Day 3)

Dec 17, 2018


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

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

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

源码LResNet50E-IR网络结构代码:https://github.com/deepinsight/insightface/blob/master/src/symbols/fresnet.py

源码训练代码:https://github.com/deepinsight/insightface/blob/master/src/train_softmax.py

TF训练网络代码:https://github.com/auroua/InsightFace_TF/blob/master/nets/L_Resnet_E_IR.py

训练集:MS-Celeb-1M

验证集:LFW

注1:原作者的深度学习框架是MXNet,本篇后面有原作者提供的第三方人士使用TF开发的源码的分析。这里主要学习原作者的搭建思路。

注2:这里只关注LResNet50E-IR的网络结构的搭法。

ArcFace MXNet源码分析

参数列表

def parse_args():
  parser = argparse.ArgumentParser(description='Train face network')
  # general
  parser.add_argument('--data-dir', default='', help='training set directory')
  parser.add_argument('--prefix', default='../model/model', help='directory to save model.')
  parser.add_argument('--pretrained', default='', help='pretrained model to load')
  parser.add_argument('--ckpt', type=int, default=1, help='checkpoint saving option. 0: discard saving. 1: save when necessary. 2: always save')
  parser.add_argument('--loss-type', type=int, default=4, help='loss type')
  parser.add_argument('--verbose', type=int, default=2000, help='do verification testing and model saving every verbose batches')
  parser.add_argument('--max-steps', type=int, default=0, help='max training batches')
  parser.add_argument('--end-epoch', type=int, default=100000, help='training epoch size.')
  parser.add_argument('--network', default='r50', help='specify network')
  parser.add_argument('--image-size', default='112,112', help='specify input image height and width')
  parser.add_argument('--version-se', type=int, default=0, help='whether to use se in network')
  parser.add_argument('--version-input', type=int, default=1, help='network input config')
  parser.add_argument('--version-output', type=str, default='E', help='network embedding output config')
  parser.add_argument('--version-unit', type=int, default=3, help='resnet unit config')
  parser.add_argument('--version-multiplier', type=float, default=1.0, help='filters multiplier')
  parser.add_argument('--version-act', type=str, default='prelu', help='network activation config')
  parser.add_argument('--use-deformable', type=int, default=0, help='use deformable cnn in network')
  parser.add_argument('--lr', type=float, default=0.1, help='start learning rate')
  parser.add_argument('--lr-steps', type=str, default='', help='steps of lr changing')
  parser.add_argument('--wd', type=float, default=0.0005, help='weight decay')
  parser.add_argument('--fc7-wd-mult', type=float, default=1.0, help='weight decay mult for fc7')
  parser.add_argument('--fc7-lr-mult', type=float, default=1.0, help='lr mult for fc7')
  parser.add_argument("--fc7-no-bias", default=False, action="store_true" , help="fc7 no bias flag")
  parser.add_argument('--bn-mom', type=float, default=0.9, help='bn mom')
  parser.add_argument('--mom', type=float, default=0.9, help='momentum')
  parser.add_argument('--emb-size', type=int, default=512, help='embedding length')
  parser.add_argument('--per-batch-size', type=int, default=128, help='batch size in each context')
  parser.add_argument('--margin-m', type=float, default=0.5, help='margin for loss')
  parser.add_argument('--margin-s', type=float, default=64.0, help='scale for feature')
  parser.add_argument('--margin-a', type=float, default=1.0, help='')
  parser.add_argument('--margin-b', type=float, default=0.0, help='')
  parser.add_argument('--easy-margin', type=int, default=0, help='')
  parser.add_argument('--margin', type=int, default=4, help='margin for sphere')
  parser.add_argument('--beta', type=float, default=1000., help='param for sphere')
  parser.add_argument('--beta-min', type=float, default=5., help='param for sphere')
  parser.add_argument('--beta-freeze', type=int, default=0, help='param for sphere')
  parser.add_argument('--gamma', type=float, default=0.12, help='param for sphere')
  parser.add_argument('--power', type=float, default=1.0, help='param for sphere')
  parser.add_argument('--scale', type=float, default=0.9993, help='param for sphere')
  parser.add_argument('--rand-mirror', type=int, default=1, help='if do random mirror in training')
  parser.add_argument('--cutoff', type=int, default=0, help='cut off aug')
  parser.add_argument('--color', type=int, default=0, help='color jittering aug')
  parser.add_argument('--images-filter', type=int, default=0, help='minimum images per identity filter')
  parser.add_argument('--target', type=str, default='lfw,cfp_fp,agedb_30', help='verification targets')
  parser.add_argument('--ce-loss', default=False, action='store_true', help='if output ce loss')
  args = parser.parse_args()
return args

MXNet我也不太会,但深度学习框架都有共通点,有描述错误的地方可以告知我。

参数列表包含很多东西,我们先不管别的,先看数据流是怎么一回事。

首先看跑LResNet50E-IR的命令参数:

CUDA_VISIBLE_DEVICES='0,1,2,3' python -u train_softmax.py --network r50 --loss-type 2 --margin-m 0.35 --data-dir ../datasets/faces_ms1m_112x112 --prefix ../model-r50-amsoftmax

第一个是用了四块GPU,网络结构是r50即ResNet50,loss-type=2即使用的CosineFace Loss,margin-m=0.35即论文中的m变量,数据集的目录是faces_ms1m_112x112这个文件夹,里面包含都是已经切割好的112×112的人脸图片,最后一个参数是预训练参数保存的位置。

来看训练代码的顶层结构,源代码地址:https://github.com/deepinsight/insightface/blob/master/src/train_softmax.py

第一步:各种参数变量的声明,比如图片的尺寸,通道数量等等。

第二步:调用get_symbols函数得到网络结构sym,因为MXNet主要是符号(Symbol)编程,网络结构由各种符号组成。

第三步:初始化FaceImageIter数据迭代器,初始化各类迭代用的参数,比如batch_size等等

第四步:初始化评价函数AccMetric

第五步:神经网络初始化,调用mx.init.Xavier,我理解是和tf.Session一样的东西

第六步:定义优化器,他定义的是SGD

第七步:开始迭代,用Batch_callback的方法迭代每个batch,在参数verbose的整数倍下计算测试集准确率并更新参数

大概的流程便是如此,其中网络的具体结构和特征是怎么提取的都在第二步的get_symbols当中体现,于是接下来开始进入网络结构。

网络结构顶层

在get_symbol函数中,大体的流程如下:

第一步:根据ResNet50的结构构建主体网络计算图,输入112×112×3的图像,输出是embedding,512维度

第二步:初始化类别空间

第三步:计算CosFace Loss,具体代码非常的少,如下所示:

# 两个参数,s和m
s = args.margin_s
m = args.margin_m
assert(s>0.0)
assert(m>0.0)
# 对X和W进行L2正则
_weight = mx.symbol.L2Normalization(_weight, mode='instance')
nembedding = mx.symbol.L2Normalization(embedding, mode='instance', name='fc1n')*s
# fc7得到的结果即为s*cos,映射到类别空间,得到各个类别的分值
fc7 = mx.sym.FullyConnected(data=nembedding, weight = _weight, no_bias = True, num_hidden=args.num_classes, name='fc7')
s_m = s*m
# 将真值映射到one-hot类别空间
gt_one_hot = mx.sym.one_hot(gt_label, depth = args.num_classes, on_value = s_m, off_value = 0.0)
# 预测得分与真值one-hot进行作差
fc7 = fc7-gt_one_hot

第四步:将fc7和真值放入传统Softmax中输出得到结果

网络结构内部

上述第一步中,如何得到embedding 512维序列我们跳过了,这里分析一下这个过程。

论文的ResNet结构代码:https://github.com/deepinsight/insightface/blob/master/src/symbols/fresnet.py

可以看到,LResNet50E-IR,传入的ResNet50的主要参数有(上述地址的最下面的那个函数):

  • 网络层数: num_layers = 50
  • 卷积层的Unit: filter_list = [64, 64, 128, 256, 512]
  • 没有1×1的filter: bottle_neck = False
  • 网络有四个部分: num_stages = 4
  • 四个部分分别是: [3, 4, 14, 3]

接下来开始搭建,继续按照步骤说明(到了resnet函数)

第一步:初始化body,代码如下:

# 初始化一个data符号
data = mx.sym.Variable(name='data')

data = mx.sym.identity(data=data, name='id')
data = data-127.5
data = data*0.0078125
body = data
# Filter=64,初始化的卷积
body = Conv(data=body, num_filter=filter_list[0], kernel=(3,3), stride=(1,1), pad=(1, 1),
                        no_bias=True, name="conv0", workspace=workspace)
body = mx.sym.BatchNorm(data=body, fix_gamma=False, eps=2e-5, momentum=bn_mom, name='bn0')
body = Act(data=body, act_type=act_type, name='relu0')

# BN+relu后初始化完成

第二步:迭代式设计网络,代码如下:

for i in range(num_stages):
	# 注意!这里的False为dim_match
	body = residual_unit(body, filter_list[i+1], (2, 2), False, name='stage%d_unit%d' % (i + 1, 1), bottle_neck=bottle_neck, **kwargs)
	
	for j in range(units[i]-1):
		# 注意!这里的True为dim_match
		body = residual_unit(body, filter_list[i+1], (1,1), True, name='stage%d_unit%d' % (i+1, j+2), bottle_neck=bottle_neck, **kwargs)

fc1 = symbol_utils.get_fc1(body, num_classes, fc_type)
return fc1

首先,我们先看这几行中最主要的residual_unit函数在干嘛。

residual_unit函数如下,由于默认version_unit为3,所以其实就运行了一行就返回:

return residual_unit_v3(data, num_filter, stride, dim_match, name, bottle_neck, **kwargs)

那么,residual_unit_v3在干嘛呢?我们看375行-403行:

  1. bn1 = 对data进行BatchNormal
  2. conv1 = 对bn1进行filter,3×3的卷积层
  3. bn2 = 对conv1进行BatchNormal
  4. act1 = prelu(bn2)
  5. conv2 = 对act1进行filter,3×3的卷积
  6. bn3 = 对conv2进行BatchNormal
  7. if dim_match==True: shortcut = data,即准备好残差中的shortcut
  8. elif dim_match!=True: conv1sc = 对data进行filter,(1×1)的卷积,shortcut=对conv1sc进行BatchNorm
  9. 最终输出bn3 + shortcut

整个residual_unit_v3的流程图可以参考我画的下面的这幅图:

image

然后这个residual_unit_v3弄清楚后,我们再来看这个迭代的网络结构:

for i in range(num_stages):
	# 注意!这里的False为dim_match,步长为(2,2)
	body = residual_unit(body, filter_list[i+1], (2, 2), False, name='stage%d_unit%d' % (i + 1, 1), bottle_neck=bottle_neck, **kwargs)
	
	for j in range(units[i]-1):
		# 注意!这里的True为dim_match,步长为(1,1)
		body = residual_unit(body, filter_list[i+1], (1,1), True, name='stage%d_unit%d' % (i+1, j+2), bottle_neck=bottle_neck, **kwargs)

fc1 = symbol_utils.get_fc1(body, num_classes, fc_type)
return fc1

我们拆开来看,num_stages=4,所以i=0,1,2,3

  • i=0的时候:
  1. body = 上述网络结构[body,filter=64,步长(2,2),dim_match=False],经历3个卷积层
  2. j进行0,1两次循环,两次进行body = 上述网络结构[body,filter=64,步长(1,1),dim_match=True],经历4个卷积层

一共经历7个卷积层

  • i=1的时候:
  1. body = 上述网络结构[body,filter=128,步长(2,2),dim_match=False],经历3个卷积层
  2. j进行0,1,2三次循环,三次进行body = 上述网络结构[body,filter=128,步长(1,1),dim_match=True],经历6个卷积层

一共经历9个卷积层

  • i=2的时候:
  1. body = 上述网络结构[body,filter=256,步长(2,2),dim_match=False],经历3个卷积层
  2. j进行0,1,2,一直到12十三次循环,十三次进行body = 上述网络结构[body,filter=256,步长(1,1),dim_match=True],经历26个卷积层

一共经历29个卷积层

  • i=3的时候:
  1. body = 上述网络结构[body,filter=512,步长(2,2),dim_match=False],经历3个卷积层
  2. j进行0,1两个循环,两次进行body = 上述网络结构[body,filter=512,步长(1,1),dim_match=True],经历4个卷积层

一共经历7个卷积层

四个步骤总共经历卷积层7+9+29+7=52个卷积层,再加上初始化的卷积层,一共有53个。

到这里网络结构是否有了一个抽象的感觉,总体的结构可以参考下面的结构:

image

里面每一个虚线都包含一个Conv(1×1×filter的卷积层)+BN,每一个Conv下面都接了一个BN

L-ResNet50E-IR的tf实现分析

源代码地址:https://github.com/auroua/InsightFace_TF/blob/master/nets/L_Resnet_E_IR.py

类与函数设计:

  • class ElementwiseLayer(Layer)
  • class BatchNormLayer(Layer)
  • ResNet50相关
    • subsample
    • conv2d_same
    • bottleneck_IR
    • resnet
      • class Block
      • resnet_v1_block
    • get_resnet

其中ResNet50相关和我第一篇记录中后面的代码类似,多了一个IR的改动

第一步:x初始化为[None, 112, 112, 3]的结构,即n个112×112×3的图片

第二步:定义四个Block块:

blocks = [
    resnetse_v1_block('block1', base_depth=64, num_units=3, stride=2, rate=1, unit_fn=unit_fn),
    resnetse_v1_block('block2', base_depth=128, num_units=4, stride=2, rate=1, unit_fn=unit_fn),
    resnetse_v1_block('block3', base_depth=256, num_units=14, stride=2, rate=1, unit_fn=unit_fn),
    resnetse_v1_block('block4', base_depth=512, num_units=3, stride=2, rate=1, unit_fn=unit_fn)
]

将x,blocks,以及其他参数输入到resnet的构建函数中构建50层ResNet

resnet函数逻辑:

  • x作为input
  • 经过一个bottle_neck层,包含一个Conv,一个BN和一个pReLU
  • 对blocks进行循环,经过52层卷积:
    • 重复类似于上述i=0,1,2,3的操作
  • 经过一个BN
  • 经过一个Dropout
  • 经过一个ReshapeLayer
  • 经过一个DenseLayer
  • 经过一个BN
  • 返回

bottleneck_IR的函数逻辑:

  • inputs作为输入
  • 判断输入输出维度是否一样
    • 一样: shortcut直接是输入
    • 不一样: shortcut 为输入1×1卷积达到同维度
  • output = BN
  • output = conv2d
  • output = BN
  • output = pReLU
  • output = conv2d
  • output = ElementwiseLayer([shortcut+output])

bottleneck_IR_SE函数逻辑,增加了一些激活的内容:

  • inputs作为输入
  • 判断输入输出维度是否一样
    • 一样: shortcut直接是输入
    • 不一样: shortcut 为输入1×1卷积达到同维度
  • output = BN
  • output = conv2d
  • output = BN
  • output = pReLU
  • output = conv2d
  • sqeeze = InputLayer(reduce_mean(output))
  • excitation1 = DenseLayer(sqeeze) # 线性激活
  • excitation2 = DenseLayer(excitation1)
  • scale = ReshapeLayer(excitation2)
  • output = ElementwiseLayer([output+scale])
  • output = ElementwiseLayer([shortcut+output])

我觉得这整体的代码逻辑非常的清晰…我们的ResNet50直接照搬感觉即可。