我制作的工程代码在这: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行:
- bn1 = 对data进行BatchNormal
- conv1 = 对bn1进行filter,3×3的卷积层
- bn2 = 对conv1进行BatchNormal
- act1 = prelu(bn2)
- conv2 = 对act1进行filter,3×3的卷积
- bn3 = 对conv2进行BatchNormal
- if dim_match==True: shortcut = data,即准备好残差中的shortcut
- elif dim_match!=True: conv1sc = 对data进行filter,(1×1)的卷积,shortcut=对conv1sc进行BatchNorm
- 最终输出bn3 + shortcut
整个residual_unit_v3的流程图可以参考我画的下面的这幅图:
然后这个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的时候:
- body = 上述网络结构[body,filter=64,步长(2,2),dim_match=False],经历3个卷积层
- j进行0,1两次循环,两次进行body = 上述网络结构[body,filter=64,步长(1,1),dim_match=True],经历4个卷积层
一共经历7个卷积层
- i=1的时候:
- body = 上述网络结构[body,filter=128,步长(2,2),dim_match=False],经历3个卷积层
- j进行0,1,2三次循环,三次进行body = 上述网络结构[body,filter=128,步长(1,1),dim_match=True],经历6个卷积层
一共经历9个卷积层
- i=2的时候:
- body = 上述网络结构[body,filter=256,步长(2,2),dim_match=False],经历3个卷积层
- j进行0,1,2,一直到12十三次循环,十三次进行body = 上述网络结构[body,filter=256,步长(1,1),dim_match=True],经历26个卷积层
一共经历29个卷积层
- i=3的时候:
- body = 上述网络结构[body,filter=512,步长(2,2),dim_match=False],经历3个卷积层
- j进行0,1两个循环,两次进行body = 上述网络结构[body,filter=512,步长(1,1),dim_match=True],经历4个卷积层
一共经历7个卷积层
四个步骤总共经历卷积层7+9+29+7=52个卷积层,再加上初始化的卷积层,一共有53个。
到这里网络结构是否有了一个抽象的感觉,总体的结构可以参考下面的结构:
里面每一个虚线都包含一个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直接照搬感觉即可。