论文题目:《RVFace: Reliable Vector Guided Softmax Loss for Face Recognition》
建议先了解下这篇文章:MVFace
随着深度卷积神经网络 (CNN) 的进步,人脸识别取得了重大进展,其中心任务是如何提高特征辨别力。 为此,已经提出了几种基于margin(例如,角度、加法和加法角度margin)的 softmax 损失函数来增加不同类别之间的特征边距。 然而,尽管取得了很大的成就,但它们主要存在四个问题:
相较于MVFace的改进:RVFace强调了semi-hard样本的重要性,而降低对噪声和离群点的关注。也就是上面的第1点。
为了克服上述缺点,本文尝试设计一种新的损失函数。 如图 1 所示,所提出的方法 RVFace 主要包含两个关键部分:(1)利用余弦相似度分布作为线索来估计嘈杂的标签;(2)强调来自剩余可靠标签的semi-hard向量进行训练。 因此,我们将它们整合到一个公式中,该公式明确指示可靠的特征向量并自适应地强调信息量(即semi-hard)向量以指导判别特征学习。 综上所述,本文的主要贡献可以归纳如下:
Original Softmax
Softmax in face recognition
Margin-based Softmax
Mining-based Softmax
Noise-based Softmax
使用噪声标签学习正成为一种越来越有效地训练深度 CNN 的技术。 它的想法是检测噪声标签,使 CNN 模型集中在标记良好的样本上,从而获得更具辨别力的特征。 最近提出了几种基于噪声的 softmax 损失 [22]、[32],它们可以总结如下:
其中 q(x) 是对样本 x 进行加权的重新加权函数。 具体来说,如果样本 x 被预测为有噪声的标签,它的重要性就会退化。 否则,它将被重新加权函数 q(x) 强调。 例如,Zhong 等人。 [22] 开发了一个二进制重新加权函数,Hu 等人。 [32] 提出了一种动态且复杂的重新加权函数来处理嘈杂的标签。 综上所述,我们在表 I 中列出了不同类型损失函数的一些典型方法。
首先,让我们回顾一下 margin-based softmax losses 的公式,即等式(3),从中我们可以总结出:
为了解决基于margin的 softmax 损失的第一个缺点,人们可以求助于基于噪声的策略 [22],[32]。 基于margin的 softmax 损失是基于训练集被良好清理的假设。 因此,将基于噪声的策略结合到基于margin的 softmax 损失中可以在一定程度上减轻噪声标签。 直接整合它们的朴素动机可以表述为:
公式(6) 确实通过重新加权函数 q(x) 解决了噪声标签,但其改进在实践中并不令人满意。 这背后的原因可能是,重新加权函数 q(x) 很难准确预测,并且基于margin的 softmax 损失没有考虑判别训练的信息特征。
为了解决基于margin的 softmax 损失的第二个缺点,可以求助于困难样本挖掘策略 [34]、[35]。 基于挖掘的损失函数旨在关注在hard samples上训练,而margin-based loss functions是为了扩大不同类别之间的特征间隔。 因此,这两个分支是正交的,可以无缝地相互结合, 直接整合它们的朴素动机可以表述为:
、
公式方程 (7) 确实涉及指示函数 g(x) 的信息特征,但其改进在实践中有限。 这背后的原因可能是,对于 HM-Softmax [34],它明确指出了困难的例子,但它丢弃了简单的例子。 对于 Focal-Softmax [35],它使用所有示例并根据经验通过调制因子对它们进行重新加权,但是hard samples对于训练来说不清楚并且没有直观的解释。 对于人脸识别,目前典型的困难样本挖掘策略对于判别学习来说通常可以忽略不计。
直觉告诉我们,考虑分离良好的特征向量对判别学习影响不大。 这意味着错误分类的特征向量对于增强特征可辨别性更为重要。 具体来说,基于 margin-based softmax 损失函数,工作 [64] 定义了一个二元指标 Ik 来自适应地指示样本(特征)在当前阶段是否被特定分类器 wk(其中 k̸= y)错误分类:
由式(8)可以看出,如果一个样本(特征)被误分类,即f(m,θwy,x)−cos(θwk,x)≤0,则暂时强调。 因此,错误分类向量引导 Softmax (MV-Softmax) [64] 损失公式如下:
其中 h(t, θwk,x, Ik) ≥ 1 是重新加权函数,用于强调指示的错误分类向量。 具体定义如下:
其中 t ≥ 0 是预设的超参数。 不幸的是,MV-Softmax 损失的成功在很大程度上也取决于标注良好的训练数据集。
如上所述,MV-Softmax 损失的错误分类向量(例如,图 2 中的红色、橙色和蓝色点)可能带有噪声标签。 幸运的是,根据文献[11]、[32],通常表明余弦相似度较小的样本有较大概率是噪声标签。 这种现象主要是因为 CNN 可以快速记住简单/干净的样本,也可以记住困难/嘈杂的样本 [63]。 受此启发,我们开始开发可靠的向量引导 softmax 损失。 如图 2 所示,如果错误分类的向量 x4(蓝点)与它们的真实类别 w1 相距较远(即其余弦相似度较小),则很可能是噪声标签。 为此,我们可以采用此提示来检测噪声标签。 具体来说,我们定义检测函数 d(x) 来预测噪声标签,如下所示:
其中 τ 是确定噪声标签的阈值。 特别是,随着 CNN 迭代更新其参数,确定噪声标签的阈值 τ 应该是动态的。 如[11]所示,余弦相似度的分布是双峰分布。 因此,我们可以求助于 OTSU 算法 [65] 来确定阈值。 从定义方程式。 (11),我们可以看出,如果样本 x 的余弦相似度 cos(θwy,x) 小于阈值 τ ,则它被检测为噪声标签。 对于剩余的样本,可以进一步分为三类:easy vectors(即绿点,f(m, θwy,x) > cos(θwk,x)),semi-hard vectors(即红点,f (m, θwy,x) ≤ cos(θwk,x) ≤ cos(θwy,x)) 和模糊向量(即橙色点,cos(θwk,x) > cos(θwy,x))。 简单的向量对于判别学习来说可以忽略不计,而模糊的向量可能仍然是嘈杂的标签。 因此,我们强调semi-hard向量的重要性。 相应地,我们定义指标函数 Jk 如下:
因此,我们的 Reliable Vector guided Softmax loss (RVFace) 可以表示如下 L9 :=
显然,当训练集干净 d(x) = 1 且超参数 t = 0 时,设计的 RVFace Eq. (13) 变得与原始的基于margin的 softmax 损失等式相同。 (3).
从左到右:来自具有不同噪声率的数据集 CASIA-WebFace-R-N1、CASIAWebFace-R-N2、CASIA-WebFace-R-N3 和 CASIA-WebFace-R-N4 的所有正对的余弦相似度分布, 分别。 从直方图中,我们可以看到嘈杂的数据集呈现双峰分布。 此外,左侧人脸的余弦相似度很可能是噪声标签。
来自数据集 CASIA-WebFace-R 的所有正对的余弦相似度分布。 从直方图分布,我们可以看出 CASIA-WebFace-R 可以被视为无噪声数据集。
从左到右:所有正对余弦相似度分布以及我们在 CASIA-WebFace-R-N4 上使用不同训练时期(1、10、20、30)的 RV-AM-Softmax 损失的噪声检测的相应精度和召回率。
从左到右:我们在不同训练时期的噪声检测策略的精确度和召回率。
在测试集 LFW、SLLFW、CALFW、CPLFW、AGEDB 和 CFP 上使用不同策略验证我们的 RVFACE(RV-ARC-SOFTMAX 和 RV-AM-SOFTMAX)损失函数的性能(%)。
我们的 RVFACE(RV-ARC-SOFTMAX 和 RV-AM-SOFTMAX)在不同超参数 t 下的验证性能(%)。 ‘-’ 表示该方法无法收敛。
在测试集 LFW、BLUFR(1E-4) 和 SLLFW 上使用不同架构验证我们的 RVFACE (RV-AM-SOFTMAX) 损失函数的性能 (%)。 训练集是 CASIA-WEBFACE-R-N4。
我们 RVFace 的收敛。 从曲线中,我们得出结论,我们的 RV-Arc-Softmax 和 RV-AM-Softmax 具有良好的收敛性。
不同损失函数在测试集 LFW、SLLFW、CALFW、CPLFW、AGEDB 和 CFP 上的验证性能(%)。
不同损失函数在测试集RFW上的验证性能(%)
从左到右:MegaFace Set 1 上具有 1M 干扰项的不同损失函数的 CMC 曲线和 ROC 曲线。
不同损失函数在 MEGAFACE CHALLENGE 上的表现(%)。
来自 vanilla CASIA-WebFace 数据集的所有正对的余弦相似度分布。 从直方图分布,我们可以看出 vanilla CASIA-WebFace 是一个带有噪声标签的数据集。
香草 CASIA-WebFace [44] 中估计噪声标签的示例。 我们随机选择几个用我们的方法 RVFace 预测带有噪声标签的人。 每个人的估计噪声标签用红色虚线框表示。
测试集 MEGAFACE 挑战中不同损失函数的性能 (%)。 训练集是 VANILLA CASIA-WEBFACE。
本文针对人脸识别任务提出了一种简单但非常有效的损失函数,即 Reliable Vector(即 RVFace)引导的 softmax 损失。 具体来说,RVFace 显式估计噪声标签并自适应地强调来自剩余可靠特征向量的semi-hard特征向量以进行判别训练。 因此,它在语义上将基于特征的噪声标签检测、特征挖掘和特征余量的动机继承到一个统一的损失函数中。 因此,它表现出比基线 Softmax 损失、当前基于噪声、基于挖掘、基于边缘的损失、它们的原始融合和几种最先进的方法更高的性能。 对各种人脸识别基准的广泛实验已经验证了我们的新方法相对于最先进的替代方法的有效性。 请注意,我们方法的噪声标签检测和semi-hard向量挖掘是为基于 softmax 的损失而设计的。 它们不能直接用于度量学习损失,例如对比损失和三元组损失。
pytorch代码:
class RVArcFace(nn.Module):
# Reliable Vector Guided Softmax Loss
def __init__(self, in_features, out_features, device_id=None, s = 32.0, m = 0.35, t = 0.15, easy_margin = False, fp16 = False):
super(RVArcFace, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.device_id = device_id
self.s = s
self.m = m
self.t = t
self.weight = Parameter(torch.FloatTensor(out_features, in_features))
nn.init.xavier_uniform_(self.weight)
self.easy_margin = easy_margin
self.cos_m = math.cos(m)
self.sin_m = math.sin(m)
self.th = math.cos(math.pi - m)
self.mm = math.sin(math.pi - m) * m
self.fp16 = fp16
def forward(self, input, label):
# --------------------------- cos(theta) & phi(theta) ---------------------------
if self.device_id == None:
cos_theta = F.linear(F.normalize(input), F.normalize(self.weight))
print('cos_theta_None:{}'.format(cos_theta.shape))
else:
x = input
sub_weights = torch.chunk(self.weight, len(self.device_id), dim=0)
temp_x = x.cuda(self.device_id[0])
weight = sub_weights[0].cuda(self.device_id[0])
cos_theta = F.linear(F.normalize(temp_x), F.normalize(weight))
for i in range(1, len(self.device_id)):
temp_x = x.cuda(self.device_id[i])
weight = sub_weights[i].cuda(self.device_id[i])
cos_theta = torch.cat((cos_theta, F.linear(F.normalize(temp_x), F.normalize(weight)).cuda(self.device_id[0])), dim=1)
cos_theta = cos_theta.clamp(-1, 1)
batch_size = label.size(0)
gt = cos_theta[torch.arange(0, batch_size), label].view(-1, 1)
sin_theta = torch.sqrt(1.0 - torch.pow(gt, 2))
cos_theta_m = gt * self.cos_m - sin_theta * self.sin_m
mask1 = cos_theta >= cos_theta_m
mask2 = cos_theta <= gt
mask = mask1 & mask2 #cos(ɵy+m) <= cosɵj <= cosɵy
hard_vector = cos_theta[mask]
cos_theta[mask] = (self.t + 1) * hard_vector + self.t
if self.fp16:
cos_theta_m = cos_theta_m.half()
if self.easy_margin:
final_gt = torch.where(gt > 0.0, cos_theta_m, gt)
else:
final_gt = torch.where(gt > self.th, cos_theta_m, gt - self.mm)
if self.device_id != None:
cos_theta = cos_theta.cuda(self.device_id[0])
cos_theta.scatter_(1, label.data.view(-1, 1), final_gt)
cos_theta *= self.s
return cos_theta
class RVCosFace(nn.Module):
# Reliable Vector Guided Softmax Loss
def __init__(self, in_features, out_features, device_id=None, s=32.0, m=0.35, t=0.15, easy_margin=False,
fp16=False):
super(RVCosFace, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.device_id = device_id
self.s = s
self.m = m
self.t = t
self.weight = Parameter(torch.FloatTensor(out_features, in_features))
nn.init.xavier_uniform_(self.weight)
self.easy_margin = easy_margin
self.cos_m = math.cos(m)
self.sin_m = math.sin(m)
self.th = math.cos(math.pi - m)
self.mm = math.sin(math.pi - m) * m
self.fp16 = fp16
def forward(self, input, label):
# --------------------------- cos(theta) & phi(theta) ---------------------------
if self.device_id == None:
cos_theta = F.linear(F.normalize(input), F.normalize(self.weight))
else:
x = input
sub_weights = torch.chunk(self.weight, len(self.device_id), dim=0)
temp_x = x.cuda(self.device_id[0])
weight = sub_weights[0].cuda(self.device_id[0])
cos_theta = F.linear(F.normalize(temp_x), F.normalize(weight))
for i in range(1, len(self.device_id)):
temp_x = x.cuda(self.device_id[i])
weight = sub_weights[i].cuda(self.device_id[i])
cos_theta = torch.cat(
(cos_theta, F.linear(F.normalize(temp_x), F.normalize(weight)).cuda(self.device_id[0])), dim=1)
cos_theta = cos_theta.clamp(-1, 1)
batch_size = label.size(0)
gt = cos_theta[torch.arange(0, batch_size), label].view(-1, 1)
# pos vectors
cos_theta_m = gt - self.m
# semi-hard vectors
mask1 = cos_theta >= cos_theta_m
mask2 = cos_theta <= gt
mask = mask1 & mask2
hard_vector = cos_theta[mask]
cos_theta[mask] = (self.t + 1.0) * hard_vector + self.t
if self.fp16:
cos_theta_m = cos_theta_m.half()
'''
if self.easy_margin:
final_gt = torch.where(gt > 0.0, cos_theta_m, gt)
else:
final_gt = torch.where(gt > self.th, cos_theta_m, gt - self.mm)
'''
final_gt = cos_theta_m
if self.device_id != None:
cos_theta = cos_theta.cuda(self.device_id[0])
cos_theta.scatter_(1, label.data.view(-1, 1), final_gt)
cos_theta *= self.s
return cos_theta