以下链接是个人关于insightFace所有见解,如有错误欢迎大家指出,我会第一时间纠正,如有兴趣可以加微信:17575010159 相互讨论技术。
人脸识别0-00:insightFace目录:https://blog.csdn.net/weixin_43013761/article/details/99646731:
这是本人项目的源码:https://github.com/944284742/1.FaceRecognition
其中script目录下的文件为本人编写,主要用于适应自己的项目,可以查看该目录下的redeme文件。
在讲解arcface loss之前,我们必须去看一下softmax家族发展的历史,这里就直接贴出链接了,大家去看即可:
softmax变种或增强1:Large-Margin Softmax Loss for Convolutional Neural Networks
softmax增强: A-SoftMax. SphereFace: Deep Hypersphere Embedding for Face Recognition
论文阅读之Arcface:https://blog.csdn.net/Wuzebiao2016/article/details/81839452
ArcFace算法笔记:https://blog.csdn.net/u014380165/article/details/80645489
人脸识别-arcface损失函数:看了前面的,再看这篇,通俗易解。
大家看了上面的之后,应该思路也比较清晰了。在这里我也讲解一下自己的理解。如果我们要了解arcface,那么肯定需要了解一下该算法的发展,演变过程,
首先我们来看看
这是传统的的Softmax公式, W x T x i W_x^Tx_i WxTxi+ b j b_j bj代表的是全链接的输出,通过计算会得到每个类别的概率。而这种方式主要考虑是否能够正确的分类,缺乏类内和类间距的约束。在[A Discriminative Feature Learning Approach for Deep Face Recognition]这篇文章中,作者使用了一个比LeNet更深的网络结构,用Mnist做了一个小实验来证明Softmax学习到的特征与理想状态下的差距:
实验结果表明,传统的Softmax仍存在着很大的类内距离,也就是说,通过对损失函数增加类内距离的约束,能达到比更新现有网络结构更加事半功倍的效果。于是,[A Discriminative Feature Learning Approach for Deep Face Recognition]的作者提出了Center Loss,并从不同角度对结果的提升做了论证。
下面是直接的理解:
N是样本的数量,i代表第i个样本,j代表第j个类别,fyi代表着第i个样本所属的类别的分数
fyi是全连接层的输出,代表着每一个类别的分数,
每一个分数即为权重W和特征向量X的内积
每个样本的softmax值即为:
假设一个2分类问题,x属于类别1,那么原来的softmax肯定是希望:
W 1 T x > W 2 T x W^T_1x > W^T_2x W1Tx>W2Tx
也就是属于类别1的概率大于类别2的概率,这个式子和下式是等效的:
∣ ∣ W 1 ∣ ∣ ∣ ∣ x ∣ ∣ c o s ( θ 1 ) > ∣ ∣ W 2 ∣ ∣ ∣ ∣ x ∣ ∣ c o s ( θ 2 ) ||W1|| ||x||cos(θ _1) > ||W2|| ||x||cos(θ _2) ∣∣W1∣∣∣∣x∣∣cos(θ1)>∣∣W2∣∣∣∣x∣∣cos(θ2)
large margin softmax就是将上面不等式替换为:
∣ ∣ W 1 ∣ ∣ ∣ ∣ x ∣ ∣ c o s ( m θ 1 ) > ∣ ∣ W 2 ∣ ∣ ∣ ∣ x ∣ ∣ c o s ( θ 2 ) ( 0 < θ 1 < p i = 3.14 m ) ||W1|| ||x||cos(mθ _1) > ||W2|| ||x||cos(θ _2)(0<θ _1<\frac{pi=3.14}{m}) ∣∣W1∣∣∣∣x∣∣cos(mθ1)>∣∣W2∣∣∣∣x∣∣cos(θ2)(0<θ1<mpi=3.14)
m是正整数,cos函数在0到π范围又是单调递减的,所以cos(mx)要小于cos(x)。通过这种方式定义损失会逼得模型学到类间距离更大的,类内距离更小的特征。
从几何的角度看两种损失的差别:
设置为cos(mx)后,使得学习到的W参数更加的扁平,可以加大样本的类间距离。
Large-Margin Softmax的实验效果:
Center Loss的整体思想是希望一个batch中每个样本的feature你feature的中心的距离的平方和要越小越好,也就是类内距离越小越好。作者提出,最终的损失函数包含softmax loss和center loss,用参数λ来控制二者的比重,如下面公式所示:
因而,加入了Softmax Loss对正确类别分类的考虑以及Center Loss对类内距离紧凑的考虑,总的损失函数在分类结果上有很好的表现力。以下是作者继上个实验后使用新的损失函数并调节不同的参数λ \lambdaλ得到的实验结果,可以看到,加入了Center Loss后增加了对类内距离的约束,使得同个类直接的样本的类内特征距离变得紧凑。
A-softmax loss简单讲就是在large margin softmax loss的基础上添加了两个限制条件||W||=1和b=0,使得预测仅取决于W和x之间的角度。
softmax的计算:
W i T x + b i W^T_ix + b_i WiTx+bi可以改写成 ∣ ∣ W 1 ∣ ∣ ∣ ∣ x ∣ ∣ c o s ( θ i ) + b i ||W1|| ||x||cos(θ _i)+b_i ∣∣W1∣∣∣∣x∣∣cos(θi)+bi
若引入两个限制条件,
∣ ∣ W 1 ∣ ∣ = ∣ ∣ W 2 ∣ ∣ = 1 ||W1|| = ||W2|| = 1 ∣∣W1∣∣=∣∣W2∣∣=1
以及
b 1 = b 2 = 0 b_1=b_2=0 b1=b2=0
decision boundary变为:
∣ ∣ x ∣ ∣ ( c o s θ 1 − c o s θ 2 ) = 0 ||x||(cosθ_1 - cosθ_2) = 0 ∣∣x∣∣(cosθ1−cosθ2)=0,只取决于角度了
则损失函数变为:
在这两个限制条件的基础上,作者又添加了和large margin softmax loss一样的角度参数,使得公式变为:
在A-softmax的基础上,修改Cos(mθ)为一个新函数:
与ASoftmax中定的的类似,可以达到减小对应标签项的概率,增大损失的效果,因此对同一类的聚合更有帮助
然后根据Normface,对f进行归一化,乘上缩放系数s,最终的损失函数变为:
这样做的好处在于A-Softmax的倍角计算是要通过倍角公式,反向传播时不方便求导,而只减m反向传播时导数不用变化
Asoftmax是用m乘以θ,而AMSoftmax是用cosθ减去m,这是两者的最大不同之处:一个是角度距离,一个是余弦距离。
之所以选择cosθ-m而不是cos(θ-m),这是因为我们从网络中得到的是W和f的内积,如果要优化cos(θ-m)那么会涉及到arccos操作,计算量过大。
分类正确label的值为
,
cos函数在(0,1)内是单调递减少的,加上m,会使该值变得更小,从而loss会变得很大。这样修改的原因:角度距离比余弦距离在对角度的影响更加直接
现在已经对softmax到arcface的发展史,有了大致的了解。下面看代码recognition/train.py(注意:fc7输出的就是角度θ):
def get_symbol(args):
# 获得一个特征向量
embedding = eval(config.net_name).get_symbol()
# 定义一个标签的占位符,用来存放标签
all_label = mx.symbol.Variable('softmax_label')
gt_label = all_label
is_softmax = True
# 如果损失函数为softmax
if config.loss_name == 'softmax':
# 定义一个全连接层的权重,使用全局池化代替全链接层
_weight = mx.symbol.Variable("fc7_weight", shape=(config.num_classes, config.emb_size),
lr_mult=config.fc7_lr_mult, wd_mult=config.fc7_wd_mult, init=mx.init.Normal(0.01))
# 如果不设置bias,使用全局池化代替全链接层,得到每个id的概率值
if config.fc7_no_bias:
fc7 = mx.sym.FullyConnected(data=embedding, weight=_weight, no_bias=True, num_hidden=config.num_classes,
name='fc7')
# 如果设置_bias,使用全局池化代替全链接层,得到每个id的cos_t
else:
_bias = mx.symbol.Variable('fc7_bias', lr_mult=2.0, wd_mult=0.0)
fc7 = mx.sym.FullyConnected(data=embedding, weight=_weight, bias=_bias, num_hidden=config.num_classes,
name='fc7')
# 如果损失函数为margin_softmax
elif config.loss_name == 'margin_softmax':
# 定义一个全连接层的权重,使用全局池化代替全链接层
_weight = mx.symbol.Variable("fc7_weight", shape=(config.num_classes, config.emb_size),
lr_mult=config.fc7_lr_mult, wd_mult=config.fc7_wd_mult, init=mx.init.Normal(0.01))
# 获得loss中m的缩放系数
s = config.loss_s
# 先进行L2正则化,然后进行全链接
_weight = mx.symbol.L2Normalization(_weight, mode='instance')
nembedding = mx.symbol.L2Normalization(embedding, mode='instance', name='fc1n') * s
#使用全局池化代替全链接层,得到每个id的角度*64
fc7 = mx.sym.FullyConnected(data=nembedding, weight=_weight, no_bias=True, num_hidden=config.num_classes,
name='fc7')
in_shape,out_shape,uax_shape = fc7.infer_shape(data = (2,3,112,112))
print('fc7',out_shape)
# 其存在m1,m2,m3是为了把算法整合在一起,
# arcface cosface combined
if config.loss_m1 != 1.0 or config.loss_m2 != 0.0 or config.loss_m3 != 0.0:
# cosface loss
if config.loss_m1 == 1.0 and config.loss_m2 == 0.0:
s_m = s * config.loss_m3
gt_one_hot = mx.sym.one_hot(gt_label, depth=config.num_classes, on_value=s_m, off_value=0.0)
fc7 = fc7 - gt_one_hot
# arcface combined
else:
# fc7每一行找出gt_label对应的值,即 角度*s
zy = mx.sym.pick(fc7, gt_label, axis=1)
in_shape,out_shape,uax_shape = zy.infer_shape(data = (2,3,112,112),softmax_label = (2,))
print('zy', out_shape)
# 进行复原,前面乘以了s,cos_t为-1到1之间
cos_t = zy / s
# t为0-3.14之间
# 该arccos是为了让后续的cos单调递增
t = mx.sym.arccos(cos_t)
# m1 sphereface
if config.loss_m1 != 1.0:
t = t * config.loss_m1
# arcface或者combined
if config.loss_m2 > 0.0:
t = t + config.loss_m2
# t为0-3.14之间,单调递增
body = mx.sym.cos(t)
# combined 或者 arcface
if config.loss_m3 > 0.0:
body = body - config.loss_m3
new_zy = body * s
# 得到差值
diff = new_zy - zy
# 扩展一个维度
diff = mx.sym.expand_dims(diff, 1)
# 把标签转化为one_hot编码
gt_one_hot = mx.sym.one_hot(gt_label, depth=config.num_classes, on_value=1.0, off_value=0.0)
# 进行更新
body = mx.sym.broadcast_mul(gt_one_hot, diff)
fc7 = fc7 + body
# 如果损失函数为triplet
elif config.loss_name.find('triplet') >= 0:
is_softmax = False
nembedding = mx.symbol.L2Normalization(embedding, mode='instance', name='fc1n')
anchor = mx.symbol.slice_axis(nembedding, axis=0, begin=0, end=args.per_batch_size // 3)
positive = mx.symbol.slice_axis(nembedding, axis=0, begin=args.per_batch_size // 3,
end=2 * args.per_batch_size // 3)
negative = mx.symbol.slice_axis(nembedding, axis=0, begin=2 * args.per_batch_size // 3, end=args.per_batch_size)
if config.loss_name == 'triplet':
ap = anchor - positive
an = anchor - negative
ap = ap * ap
an = an * an
ap = mx.symbol.sum(ap, axis=1, keepdims=1) # (T,1)
an = mx.symbol.sum(an, axis=1, keepdims=1) # (T,1)
triplet_loss = mx.symbol.Activation(data=(ap - an + config.triplet_alpha), act_type='relu')
triplet_loss = mx.symbol.mean(triplet_loss)
else:
ap = anchor * positive
an = anchor * negative
ap = mx.symbol.sum(ap, axis=1, keepdims=1) # (T,1)
an = mx.symbol.sum(an, axis=1, keepdims=1) # (T,1)
ap = mx.sym.arccos(ap)
an = mx.sym.arccos(an)
triplet_loss = mx.symbol.Activation(data=(ap - an + config.triplet_alpha), act_type='relu')
triplet_loss = mx.symbol.mean(triplet_loss)
triplet_loss = mx.symbol.MakeLoss(triplet_loss)
out_list = [mx.symbol.BlockGrad(embedding)]
# 如果使用了softmax
if is_softmax:
softmax = mx.symbol.SoftmaxOutput(data=fc7, label=gt_label, name='softmax', normalization='valid')
out_list.append(softmax)
if config.ce_loss:
# ce_loss = mx.symbol.softmax_cross_entropy(data=fc7, label = gt_label, name='ce_loss')/args.per_batch_size
body = mx.symbol.SoftmaxActivation(data=fc7)
body = mx.symbol.log(body)
_label = mx.sym.one_hot(gt_label, depth=config.num_classes, on_value=-1.0, off_value=0.0)
body = body * _label
ce_loss = mx.symbol.sum(body) / args.per_batch_size
out_list.append(mx.symbol.BlockGrad(ce_loss))
# 如果是triplet
else:
out_list.append(mx.sym.BlockGrad(gt_label))
out_list.append(triplet_loss)
# 聚集所有的符号
out = mx.symbol.Group(out_list)
return out
好了,这次就分析到此为止了