聪明的人脸识别5——Tensorflow2 搭建自己的Facenet人脸识别平台

学习前言

最近又学了我最喜欢的retinaface,以前也了解过facenet,但是没有训练过,可以去学习一下!
在这里插入图片描述

什么是Facenet

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

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

测试时只需要计算人脸特征EMBEDDING,然后计算距离使用阈值即可判定两张人脸照片是否属于相同的个体。
在这里插入图片描述
简单来讲,在使用阶段,facenet即是:
1、输入一张人脸图片
2、通过深度卷积网络提取特征
3、L2标准化
4、得到一个长度为128特征向量。

源码下载

https://github.com/bubbliiiing/facenet-tf2

Facenet的实现思路

一、预测部分

1、主干网络介绍

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

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

MobilenetV1模型是Google针对手机等嵌入式设备提出的一种轻量级的深层神经网络,其使用的核心思想便是depthwise separable convolution(深度可分离卷积块)。

深度可分离卷积块由两个部分组成,分别是深度可分离卷积和1x1普通卷积,深度可分离卷积的卷积核大小一般是3x3的,便于理解的话我们可以把它当作是特征提取,1x1的普通卷积可以完成通道数的调整。

下图为深度可分离卷积块的结构示意图:
在这里插入图片描述
深度可分离卷积块的目的是使用更少的参数来代替普通的3x3卷积。

我们可以进行一下普通卷积和深度可分离卷积块的对比:

对于普通卷积而言,假设有一个3×3大小的卷积层,其输入通道为16、输出通道为32。具体为,32个3×3大小的卷积核会遍历16个通道中的每个数据,最后可得到所需的32个输出通道,所需参数为16×32×3×3=4608个。

对于深度可分离卷积结构块而言,假设有一个深度可分离卷积结构块,其输入通道为16、输出通道为32,其会用16个3×3大小的卷积核分别遍历16通道的数据,得到了16个特征图谱。在融合操作之前,接着用32个1×1大小的卷积核遍历这16个特征图谱,所需参数为16×3×3+16×32×1×1=656个。

可以看出来深度可分离卷积结构块可以减少模型的参数。

如下就是MobileNet的结构,其中Conv dw就是分层卷积,在其之后都会接一个1x1的卷积进行通道处理,
在这里插入图片描述

在这里插入图片描述

from tensorflow.keras import backend as K
from tensorflow.keras.layers import (Activation, BatchNormalization,
                                     Conv2D, Dense, DepthwiseConv2D, Dropout,
                                     GlobalAveragePooling2D)
from tensorflow.keras.models import Model


def _conv_block(inputs, filters, kernel=(3, 3), strides=(1, 1)):
    x = Conv2D(filters, kernel,
               padding='same',
               use_bias=False,
               strides=strides,
               name='conv1')(inputs)
    x = BatchNormalization(name='conv1_bn')(x)
    return Activation(relu6, name='conv1_relu')(x)


def _depthwise_conv_block(inputs, pointwise_conv_filters, depth_multiplier=1, strides=(1, 1), block_id=1):

    x = DepthwiseConv2D((3, 3),
                        padding='same',
                        depth_multiplier=depth_multiplier,
                        strides=strides,
                        use_bias=False,
                        name='conv_dw_%d' % block_id)(inputs)

    x = BatchNormalization(name='conv_dw_%d_bn' % block_id)(x)
    x = Activation(relu6, name='conv_dw_%d_relu' % block_id)(x)

    x = Conv2D(pointwise_conv_filters, (1, 1),
               padding='same',
               use_bias=False,
               strides=(1, 1),
               name='conv_pw_%d' % block_id)(x)
    x = BatchNormalization(name='conv_pw_%d_bn' % block_id)(x)
    return Activation(relu6, name='conv_pw_%d_relu' % block_id)(x)

def relu6(x):
    return K.relu(x, max_value=6)

def MobileNet(inputs, embedding_size=128, dropout_keep_prob=0.4, alpha=1.0, depth_multiplier=1):
    # 160,160,3 -> 80,80,32
    x = _conv_block(inputs, 32, strides=(2, 2))
    
    # 80,80,32 -> 80,80,64
    x = _depthwise_conv_block(x, 64, depth_multiplier, block_id=1)

    # 80,80,64 -> 40,40,128
    x = _depthwise_conv_block(x, 128, depth_multiplier, strides=(2, 2), block_id=2)
    x = _depthwise_conv_block(x, 128, depth_multiplier, block_id=3)

    # 40,40,128 -> 20,20,256
    x = _depthwise_conv_block(x, 256, depth_multiplier, strides=(2, 2), block_id=4)
    x = _depthwise_conv_block(x, 256, depth_multiplier, block_id=5)

    # 20,20,256 -> 10,10,512
    x = _depthwise_conv_block(x, 512, depth_multiplier, strides=(2, 2), block_id=6)
    x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=7)
    x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=8)
    x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=9)
    x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=10)
    x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=11)

    # 10,10,512 -> 5,5,1024
    x = _depthwise_conv_block(x, 1024, depth_multiplier, strides=(2, 2), block_id=12)
    x = _depthwise_conv_block(x, 1024, depth_multiplier, block_id=13)

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

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

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

x = GlobalAveragePooling2D()(x)
x = Dropout(1.0 - dropout_keep_prob, name='Dropout')(x)
x = Dense(classes, use_bias=False, name='Bottleneck')(x)
x = BatchNormalization(momentum=0.995, epsilon=0.001, scale=False,
                      name='BatchNorm_Bottleneck')(x)

3、l2标准化

在这里插入图片描述
在获得一个长度为128的特征向量后,我们还需要进行l2标准化的处理。

这个L2标准化是为了使得不同人脸的特征向量可以属于同一数量级,方便比较。

在进行l2标准化前需要首先计算2-范数:
∣ ∣ x ∣ ∣ 2 = ∑ i = 1 N x i 2 ||\textbf{x}||_2 =\sqrt{\sum_{i=1}^Nx_i^2} x2=i=1Nxi2 ,也就是欧几里得范数,即向量元素绝对值的平方和再开方。

L2标准化就是每个元素/L2范数;

在keras代码中,只需要一行就可以实现l2标准化的层。

x= Lambda(lambda  x: K.l2_normalize(x, axis=-1))(x)
# 创建模型
model = Model(inputs, x, name='inception_resnet_v1')

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

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

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

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

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

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

构建代码如下,当我们在进行网络的训练的时候,可使用分类器辅助训练,在预测的时候,分类器是不需要的:

def facenet(input_shape, num_classes=None, backbone="mobilenet", mode="train"):
    inputs = Input(shape=input_shape)
    if backbone=="mobilenet":
        model = MobileNet(inputs)
    elif backbone=="inception_resnetv1":
        model = InceptionResNetV1(inputs)
    else:
        raise ValueError('Unsupported backbone - `{}`, Use mobilenet, inception_resnetv1.'.format(backbone))

    if mode == "train":
        x = Dense(num_classes)(model.output)
        x = Activation("softmax", name = "Softmax")(x)
        combine_model = Model(inputs,[x, model.output])
        return combine_model
    elif mode == "predict":
        return model
    else:
        raise ValueError('Unsupported mode - `{}`, Use train, predict.'.format(mode))

二、训练部分

1、数据集介绍

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

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

2、LOSS组成

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

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

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

我们所使用的公式为。
L = m a x ( d ( a , p ) − d ( a , n ) + m a r g i n , 0 ) L=max(d(a,p)−d(a,n)+margin,0) L=max(d(a,p)d(a,n)+margin,0)
d(a,p)就是anchor和positive的欧几里得距离。
d(a,n)就是negative和positive的欧几里得距离。
margin是一个常数。

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

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

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

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

训练自己的Facenet人脸识别算法

博客中所使用的例子为CASIA-WebFace数据集。
在这里插入图片描述
下载数据集,放在根目录下的dataset文件夹下。在这里插入图片描述
运行根目录下的txt_annotation.py,生成训练所需的cls_train.txt。
在这里插入图片描述
cls_train.txt中每一行都存放了一张图片和它对应的类别(需要类别是因为训练时会用交叉熵损失辅助收敛。)
在这里插入图片描述

下载facenet_inception_resnetv1.h5或者facenet_mobilenet.h5放在model_data文件夹内。
在这里插入图片描述
在train.py中指定合适的模型预训练权重路径。facenet_inception_resnetv1.h5是我已经训练过的基于inception_resnetv1的facenet网络;
facenet_mobilenet.h5是我已经训练过的基于mobilenet的facenet网络。
在这里插入图片描述
运行train.py开始训练。

  • 12
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 32
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bubbliiiing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值