CV-CNN-2015:FaceNet(人脸特征向量提取、计算欧氏距离)【Triplet(三元组) Loss:最大化不同人脸的距离&最小化相同人脸的距离】【可使用Mobilenet作为特征提取网络】

《原始论文:FaceNet: A Unified Embedding for Face Recognition and Clustering》
GitHub:bubbliiiing/facenet-pytorch

一、概述

谷歌人脸识别算法,发表于 CVPR 2015,利用相同人脸在不同角度等姿态的照片下有高内聚性,不同人脸有低耦合性,提出使用 cnn + triplet mining 方法,在 LFW 数据集上准确度达到 99.63%。

通过 CNN 将人脸映射到欧式空间的特征向量上,实质上:不同图片人脸特征的距离较大;通过相同个体的人脸的距离,总是小于不同个体的人脸这一先验知识训练网络。

三、FaceNet模型介绍

测试时只需要计算人脸特征Embedding,然后计算距离使用阈值即可判定两张人脸照片是否属于相同的个体。

简单来讲,在使用阶段,FaceNet即是:

  • 1、输入一张人脸图片
  • 2、通过深度卷积网络提取特征
  • 3、 L2 \text{L2} L2 标准化
  • 4、得到一个长度为128特征向量。

1、主干网络介绍

在这里插入图片描述
FaceNet的主干网络起到提取特征的作用,原版的 FaceNetInception-ResNetV1 为主干特征提取网络。

本文一共提供了两个网络作为主干特征提取网络,分别是 Mobilenet-v1Inception-ResNetV1,二者都起到特征提取的作用,为了方便理解,本博文中会使用 Mobilenet-v1 作为主干特征提取网络。

MobilenetV1模型是Google针对手机等嵌入式设备提出的一种轻量级的深层神经网络,其使用的核心思想便是 Depthwise Separable Convolution(深度可分离卷积块)。MobilenetV1详细介绍参考:https://blog.csdn.net/u013250861/article/details/126220479

import torch.nn as nn


def conv_bn(inp, oup, stride = 1):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6()
    )
    
def conv_dw(inp, oup, stride = 1):
    return nn.Sequential(
        nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
        nn.BatchNorm2d(inp),
        nn.ReLU6(),

        nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(),
    )

class MobileNetV1(nn.Module):
    def __init__(self):
        super(MobileNetV1, self).__init__()
        self.stage1 = nn.Sequential(
            # 160,160,3 -> 80,80,32
            conv_bn(3, 32, 2), 
            # 80,80,32 -> 80,80,64
            conv_dw(32, 64, 1), 

            # 80,80,64 -> 40,40,128
            conv_dw(64, 128, 2),
            conv_dw(128, 128, 1),

            # 40,40,128 -> 20,20,256
            conv_dw(128, 256, 2),
            conv_dw(256, 256, 1),
        )
        self.stage2 = nn.Sequential(
            # 20,20,256 -> 10,10,512
            conv_dw(256, 512, 2),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
        )
        self.stage3 = nn.Sequential(
            # 10,10,512 -> 5,5,1024
            conv_dw(512, 1024, 2),
            conv_dw(1024, 1024, 1),
        )

        self.avg = nn.AdaptiveAvgPoo $\text{L2}$ d((1,1))
        self.fc = nn.Linear(1024, 1000)

    def forward(self, x):
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.avg(x)
        # x = self.model(x)
        x = x.view(-1, 1024)
        x = self.fc(x)
        return x
class Facenet(nn.Module):
    def __init__(self, backbone="mobilenet", dropout_keep_prob=0.5, embedding_size=128, num_classes=None, mode="train"):
        super(Facenet, self).__init__()
        if backbone == "mobilenet":
            self.backbone = mobilenet()
            flat_shape = 1024
        elif backbone == "inception_resnetv1":
            self.backbone = inception_resnet()
            flat_shape = 1792
        else:
            raise ValueError('Unsupported backbone - `{}`, Use mobilenet, inception_resnetv1.'.format(backbone))
        self.avg = nn.AdaptiveAvgPoo $\text{L2}$ d((1,1))
        self.Dropout = nn.Dropout(1 - dropout_keep_prob)
        self.Bottleneck = nn.Linear(flat_shape, embedding_size,bias=False)
        self.last_bn = nn.BatchNorm1d(embedding_size, eps=0.001, momentum=0.1, affine=True)
        if mode == "train":
            self.classifier = nn.Linear(embedding_size, num_classes)

    def forward(self, x):
        x = self.backbone(x)
        x = self.avg(x)
        x = x.view(x.size(0), -1)
        x = self.Dropout(x)
        x = self.Bottleneck(x)
        x = self.last_bn(x)
        x = F.normalize(x, p=2, dim=1)
        return x

    def forward_feature(self, x):
        x = self.backbone(x)
        x = self.avg(x)
        x = x.view(x.size(0), -1)
        x = self.Dropout(x)
        x = self.Bottleneck(x)
        before_normalize = self.last_bn(x)
        x = F.normalize(before_normalize, p=2, dim=1)
        return before_normalize, x

    def forward_classifier(self, x):
        x = self.classifier(x)
        return x

2、根据初步特征获得长度为128的特征向量

在这里插入图片描述
利用主干特征提取网络我们可以获得一个特征层,它的shape为 (batch_size, h, w, channels) \text{(batch\_size, h, w, channels)} (batch_size, h, w, channels),我们可以将其取全局平均池化得shape为 (batch_size, channels) \text{(batch\_size, channels)} (batch_size, channels),方便后续的处理。

我们可以将平铺后的特征层进行一个神经元个数为128的全连接。此时我们相当于利用了一个长度为128的特征向量代替输入进来的图片。这个长度为128的特征向量就是输入图片的特征浓缩。

3、 L2 \text{L2} L2 标准化

在这里插入图片描述
在获得一个长度为128的特征向量后,我们还需要进行 L2 \text{L2} L2 标准化的处理。这个 L2 \text{L2} L2 标准化是为了使得不同人脸的特征向量可以属于同一数量级,方便比较

在进行 L2 \text{L2} L2 标准化前需要首先计算2-范数,也就是欧几里得范数,即向量元素绝对值的平方和再开方:

在这里插入图片描述
在pytorch代码中,只需要一行就可以实现 L2 \text{L2} L2 标准化的层。

x = F.normalize(x, p=2, dim=1)

到这里,我们输入进来的图片,已经变成了一个经过 L2 \text{L2} L2 标准化的长度为128的特征向量了!

4、构建分类器(用于辅助Triplet Loss的收敛)

当我们完成第三步后,我们已经可以利用这个特征向量进行训练和预测了。

但是由于仅仅只是用Triplet Loss会使得整个网络难以收敛,本文结合Cross-Entropy Loss和Triplet Loss作为总体loss。

  • Triplet Loss用于进行不同人的人脸特征向量欧几里得距离的扩张,同一个人的不同状态的人脸特征向量欧几里得距离的缩小。
  • Cross-Entropy Loss用于人脸分类,具体作用是辅助Triplet Loss收敛。

想要利用Cross-Entropy Loss进行训练需要构建分类器,因此对第三步获得的结果再次进行一个全连接用于分类。

构建代码如下:

  • 当我们在进行网络的训练的时候,可使用分类器辅助训练
  • 预测的时候,分类器是不需要的:
if mode == "train":
    self.classifier = nn.Linear(embedding_size, num_classes)
def forward_classifier(self, x):
    x = self.classifier(x)
    return x

四、模型训练

1、数据集介绍

我们使用的数据集是CASIA-WebFace数据集,我已经对其进行了预处理,将其属于同一个人的图片放到同一个文件夹里面,并且进行了人脸的提取和人脸的矫正。
在这里插入图片描述
数据集里面有很多的文件夹,每一个文件夹里面存放同一个人的不同情况下的人脸。不同文件夹存放不同人的脸。

  • 这是\0000045文件夹里面的人脸,属于同一个人。
    在这里插入图片描述
  • 这是\0000099文件夹里面的人脸,属于同一个人。
    在这里插入图片描述

2、LOSS组成

FaceNet使用Triplet Loss作为loss。Triplet Loss的输入是一个三元组

  • a:anchor,基准图片获得的128维人脸特征向量
  • p:positive,与基准图片属于同一张人脸的图片获得的128维人脸特征向量
  • n:negative,与基准图片不属于同一张人脸的图片获得的128维人脸特征向量

我们可以将anchor和positive求欧几里得距离,并使其尽量小。我们可以将anchor和negative求欧几里得距离,并使其尽量大。

我们所使用的公式为:
在这里插入图片描述

  • d(a,p)就是anchor和positive的欧几里得距离;
  • d(a,n)就是anchor和negative的欧几里得距离;
  • margin是一个常数;

d(a,p)前面为正符号,所以我们期望其越来越小。d(a,n)前面为负符号,所以我们期望其越来越大。

即我们希望,同一个人的不同状态的人脸特征向量欧几里得距离小。不同人的人脸特征向量欧几里得距离大。

但是由于仅仅只是用Triplet Loss会使得整个网络难以收敛,本文结合Cross-Entropy Loss和Triplet Loss作为总体loss。

Triplet Loss用于进行不同人的人脸特征向量欧几里得距离的扩张,同一个人的不同状态的人脸特征向量欧几里得距离的缩小。Cross-Entropy Loss用于人脸分类,具体作用是辅助Triplet Loss收敛。

三元组损失(Triplet Loss)/三重损失,直接反映了作者想要在面部验证,识别和聚类中实现的目标。即,他们努力嵌入能够从图像 x x x 映射到特征空间 R d R^d Rd f ( x ) f(x) f(x)。使得相同身份的所有面部之间的平方距离(与成像条件无关)很小,而来自不同身份的一对面部图像之间的平方距离很大。

三元组损失的思想也很简单,输入是三张图片(Triplet),分别为:

  • 固定影像 A(Anchor Face)
  • 反例图像 N(Negative Face )
  • 正例图像 P(Positive Face)

A与 P为同一人,与 N 为不同人。

那么 Triplet Loss 的损失即可表示为:

∣ ∣ f ( x i a ) − f ( x i p ) ∣ ∣ 2 2 + α < ∣ ∣ f ( x i a ) − f ( x i n ) ∣ ∣ 2 2 ||f(x_i^a)-f(x_i^p)||_2^2 + \alpha < ||f(x_i^a)-f(x_i^n)||_2^2 ∣∣f(xia)f(xip)22+α<∣∣f(xia)f(xin)22

也就是说,我们要让同一个人的图片更加相互接近,而不同人的照片要互相远离。

三重损失将固定影像 A 与正例 P 之间的距离最小化了,这两者具有同样的身份,同时将固定影像 A 与反例 N 之间的距离最大化了。

如下图所示:
在这里插入图片描述
这里的问题是,模型可能学习给不同的图片做出相同的编码,这意味着距离会成为 0,不幸的是,这仍然满足三重损失函数。因为这个原因,需要加入了边际 α α α(一个超参数)来避免这种情况的发生。让 d ( A , P ) d(A,P) d(A,P) d ( N , P ) d(N,P) d(N,P) 之间总存在一个差距,这即是我们在上式所看到的 α α α

def triplet_loss(alpha=0.2):
    def _triplet_loss(y_pred, Batch_size):
        anchor, positive, negative = y_pred[:int(Batch_size)], y_pred[int(Batch_size):int(2 * Batch_size)], y_pred[int(2 * Batch_size):]

        pos_dist = torch.sqrt(torch.sum(torch.pow(anchor - positive, 2), axis=-1))  # anchor和positive的欧几里得距离
        neg_dist = torch.sqrt(torch.sum(torch.pow(anchor - negative, 2), axis=-1))  # anchor和negative的欧几里得距离

        keep_all = (neg_dist - pos_dist < alpha).cpu().numpy().flatten()  # 如果 neg_dist与pos_dist在数值上没有拉开(neg_dist - pos_dist < alpha), 则为1, 否则为0
        hard_triplets = np.where(keep_all == 1)  # 找出neg_dist与pos_dist在数值上没有拉开的索引

        pos_dist = pos_dist[hard_triplets]
        neg_dist = neg_dist[hard_triplets]

        basic_loss = pos_dist - neg_dist + alpha
        loss = torch.sum(basic_loss) / torch.max(torch.tensor(1), torch.tensor(len(hard_triplets[0])))
        return loss

    return _triplet_loss

3、训练




参考资料:
聪明的人脸识别3——Pytorch 搭建自己的Facenet人脸识别平台
GitHub:bubbliiiing/facenet-pytorch
FaceNet | 机器之心
人脸特征学习 FaceNet 简介
人脸识别系统FaceNet原理
手把手教人脸识别FaceNet实现
人脸识别经典—FaceNet

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值