本文是我的学习笔记,基于人工智能领域大佬Bubbliiiing聪明的人脸识别3——Pytorch 搭建自己的Facenet人脸识别平台
原文链接:https://blog.csdn.net/weixin_44791964/article/details/108220265
记录我复现与拓展的学习过程,万分感谢大佬的开源和无私奉献。本文部分内容来自网上搜集与个人实践。如果任何信息存在错误,欢迎读者批评指正。本文仅用于学习交流,不用作任何商业用途。
Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1
Facenet实现人脸特征比对-深度学习学习笔记-2
RetinaFace人脸检测模型-Gradio界面设计
FaceNet人脸识别模型-Gradio界面设计
Retinaface+FaceNet人脸识别系统-Gradio界面设计
LFW全称为Labeled Faces in the Wild,意为“非约束环境下的人脸图像数据集”。这是一个面向人脸识别研究而构建的数据库。
LFW数据集的一些关键特征:
LFW数据集包含训练、验证和测试三个子集。算法在测试集上进行评估。LFW的出现大大推动了人脸识别技术的发展。很多最先进的人脸识别算法都会在LFW数据集上测评其性能。
Facenet论文也是在LFW的标准测试集上报告了99.63%的识别准确率,超过了当时其他算法。
Facenet是谷歌在2015年发表在CVPR上的一种人脸识别算法。其基本思路是:
使用Facenet进行人脸识别的流程:
卷积神经网络(Convolutional Neural Network, CNN) - 一种模拟视觉神经网络结构的人工神经网络,利用卷积运算提取图像的特征信息,再通过全连接层进行分类。由卷积层和池化层堆叠构成,能够自动学习识别图像的特征。在图像识别任务上效果显著。
特征向量(Embedding) - 用卷积神经网络提取出的图像特征的向量表示,长度固定,包含了输入图像的高级语义信息。Facenet使用CNN将人脸图片映射到128维的特征向量上,这个固定长度的向量即为Embedding。
Triplet Loss - Facenet中的损失函数,目标是最小化同一人脸的特征向量距离,而最大化不同人脸的特征距离。这样可以将同一人的人脸聚集,不同人脸分割开来,利于识别。
LFW数据集 - 人脸识别领域公开的一个标准数据集,包含超过13000张面部图片,有汽人脸识别算法会在这个数据集上评测其性能。
L2标准化 - 对向量进行标准化处理,使其长度为1。这有利于不同向量之间计算距离,消除量纲影响。
阈值 - 如果两个人脸向量距离小于设定的阈值,则判断为同一人,大于阈值则判断为不同人。合理设定阈值对提高识别准确率很关键。
facenet的主干网络起到提取特征的作用,facenet可以用mobilenetv1或Inception-ResNetV1为主干特征提取网络,二者都起到特征提取的作用.
Keras API documentation
我们主要使用MobilenetV1作为主干网络,设计的一个轻量级的深层神经网络模型。它的核心思想是使用depthwise separable convolution(深度可分离卷积)来减少模型参数量和计算量。
我们先了解一些基本概念:
卷积:
卷积是一种数学运算,用于将一个函数与另一个函数进行操作,以产生一个新的函数。在图像处理中,我们可以把卷积看作是一种滤波操作。它通过在图像上滑动一个小的窗口(称为卷积核),对窗口内的像素进行加权求和,从而得到新的像素值。
新像素 = 卷积核中的权重 * 窗口内像素的加权平均值
我们可以把卷积想象成用一个小小的滤网去过滤咖啡。
比如我们有一张5x5的图片,每一个小格子是一个像素点,用数字1到5表示它的颜色:
1 1 2 3 4
2 2 3 4 5
3 3 3 4 5
4 4 4 5 5
5 5 5 5 5
现在我们定义一个3x3的滤波器,就是一个3行3列的小矩阵:
0 1 0
1 0 1
0 1 0
我们把这个滤波器放在图片上,让它从左到右、从上到下滑动,每次停在一个位置。
当它停在最上面最左边时,会覆盖图片中的:
1 1 2
2 2 3
3 3 3
对应元素相乘就是:
(1 x 0) (1 x 1) (2 x 0)
(2 x 1) (2 x 0) (3 x 1)
(3 x 0) (3 x 1) (3 x 0)
然后我们把滤波器中的数字与覆盖的图片中对应的数字分别相乘,再把 9 个乘积加起来,就可以得到一个新的数字,比如这里是18。
18就会成为输出图片中对应位置的新的像素值。
我们让滤波器继续在图片上滑动,每次输出一个乘积求和的结果,最终就可以得到一个新的图片,它保留了原图片在这个滤波器下的特征。
如果我们改变滤波器的数字的安排组合,就可以得到不同的特征。
这个过程,就像我们用不同的滤网去过滤咖啡,不同的滤网会提取咖啡中的不同成分。
所以卷积核其实就是一个提取图像特征的滤波器,经过卷积操作,可以得到代表不同特征的图像。这一技术在图像处理和机器学习中很重要,比如可以用来进行图像识别等任务。
卷积层:
卷积核:
卷积层和卷积核之间的关系:卷积层包含了多个卷积核。每个卷积核可以提取不同的特征。卷积层通过并行地使用多个卷积核,可以同时提取多个特征。每个卷积核在卷积层中滑动并与输入数据进行卷积操作,生成对应的特征图。这些特征图可以被传递给神经网络的下一层进行进一步的处理和分析。
总结起来
特征图(featuremap)
通道:
步长:
深度可分离卷积(depthwise separable convolution)
假设有一个3×3大小的卷积层,其输入通道为16、输出通道为32。具体为,32个3×3大小的卷积核会遍历16个通道中的每个数据,最后可得到所需的32个输出通道,所需参数为16×32×3×3=4608个。
应用深度可分离卷积,用16个3×3大小的卷积核分别遍历16通道的数据,得到了16个特征图谱。在融合操作之前,接着用32个1×1大小的卷积核遍历这16个特征图谱,所需参数为16×3×3+16×32×1×1=656个。
可以看出来depthwise separable convolution可以减少模型的参数。
通俗来说
普通的卷积操作中,我们的输入图片有 16 个通道(可以看成 16 层玻璃板),我们希望卷积层输出 32 个通道(特征图)。
而深度可分离卷积则是分两步刷:
这样参数数量只需要16×3×3+16×32×1×1=656个。大约减少到原来的1/7。
所以深度可分离卷积通过分解步骤,显著减少了参数量。这使得模型更加轻量化,也降低了计算量。
希望这个通俗的刷玻璃板比喻可以让你更直观地理解卷积通道和深度可分离卷积的工作原理。
如下这张图就是depthwise separable convolution的结构
在建立模型的时候,可以将卷积group设置成in_filters层实现深度可分离卷积,然后再利用1x1卷积调整channels数。
通俗地理解就是3x3的卷积核厚度只有一层,然后在输入张量上一层一层地滑动,每一次卷积完生成一个输出通道,当卷积完成后,在利用1x1的卷积调整厚度。
如下就是MobileNet的结构,其中Conv dw就是分层卷积,在其之后都会接一个1x1的卷积进行通道处理,
# nets/mobilenet.py
import torch.nn as nn
def conv_bn(inp, oup, stride=1):
'''
卷积块,组合卷积层、BN层、激活函数
参数:
inp:输入通道数
oup:输出通道数
stride:卷积层步长
返回:Sequential块
'''
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
# 3x3卷积,输入通道inp,输出通道oup,调整通道数
nn.BatchNorm2d(oup),
# BN层,针对oup通道标准化
nn.ReLU6()
# ReLU6激活函数
)
def conv_dw(inp, oup, stride=1):
'''
深度可分离卷积块
参数:
inp:输入通道数
oup:输出通道数
stride:步长
返回:Sequential块
'''
return nn.Sequential(
nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
# 3x3深度可分离卷积,分组数等于inp,即split通道,通道不变
nn.BatchNorm2d(inp),
nn.ReLU6(),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
# 1x1点卷积,调整通道数为oup
nn.BatchNorm2d(oup),
nn.ReLU6(),
)
class MobileNetV1(nn.Module):
def __init__(self):
super(MobileNetV1, self).__init__()
self.stage1 = nn.Sequential(
# 160x160x3 -> 80x80x32
conv_bn(3, 32, 2),
# 第1层卷积,步长2,图像尺寸减半
# 80x80x32 -> 80x80x64
conv_dw(32, 64, 1),
# 80x80x64 -> 40x40x128
conv_dw(64, 128, 2), # 步长2,尺寸减半
conv_dw(128, 128, 1),
# 40x40x128 -> 20x20x256
conv_dw(128, 256, 2), # 步长2,尺寸减半
conv_dw(256, 256, 1),
)
self.stage2 = nn.Sequential(
# 20x20x256 -> 10x10x512
conv_dw(256, 512, 2), # 步长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(
# 10x10x512 -> 5x5x1024
conv_dw(512, 1024, 2), # 步长2,尺寸减半
conv_dw(1024, 1024, 1),
)
self.avg = nn.AdaptiveAvgPool2d((1, 1)) # 1x1全局池化
self.fc = nn.Linear(1024, 1000) # 分类全连接层,1000类
def forward(self, x):
x = self.stage1(x)
# stage1模块
x = self.stage2(x)
# stage2模块
x = self.stage3(x)
# stage3模块
x = self.avg(x)
# 平均池化
x = x.view(-1, 1024)
# 平铺,用于全连接层输入
x = self.fc(x)
# 全连接层
return x
代码实现了MobileNetV1模型的定义和前向传播过程。
这段代码的主要功能是将输入图像通过多个阶段的深度可分离卷积块逐渐降低为更小的特征图尺寸,然后通过全局平均池化层进行空间降维,最后使用全连接层进行分类预测。这样的设计使得模型能够有效地提取图像特征,并在保持较小模型尺寸的同时实现准确的分类结果。
利用主干特征提取网络我们可以获得一个特征层,它的shape为(batch_size, h, w, channels),我们可以将其取全局平均池化,方便后续的处理(batch_size, channels)。
我们可以
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__()
# 根据选择的backbone网络初始化模型
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))
# 自适应平均池化层,将特征图转换为大小为1x1的全局特征
self.avg = nn.AdaptiveAvgPool2d((1,1))
# Dropout层,用于随机丢弃特征向量中的部分元素以减少过拟合风险
self.Dropout = nn.Dropout(1 - dropout_keep_prob)
# Bottleneck线性层,将特征映射到长度为embedding_size的特征空间
self.Bottleneck = nn.Linear(flat_shape, embedding_size,bias=False)
# BatchNorm1d层,对特征向量进行归一化处理
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):
# 数据在backbone网络中前向传播得到特征图
x = self.backbone(x)
# 自适应平均池化将特征图转换为大小为1x1的全局特征
x = self.avg(x)
# 将特征图展平成一维向量
x = x.view(x.size(0), -1)
# Dropout层对特征向量进行随机丢弃以减少过拟合风险
x = self.Dropout(x)
# Bottleneck线性层将特征映射到长度为embedding_size的特征空间
x = self.Bottleneck(x)
# BatchNorm1d层对特征向量进行归一化处理
x = self.last_bn(x)
# L2标准化使特征向量具有单位长度
x = F.normalize(x, p=2, dim=1)
return x
def forward_feature(self, x):
# 数据在backbone网络中前向传播得到特征图
x = self.backbone(x)
# 自适应平均池化将特征图转换为大小为1x1的全局特征
x = self.avg(x)
# 将特征图展平成一维向量
x = x.view(x.size(0), -1)
# Dropout层对特征向量进行随机丢弃以减少过拟合风险
x = self.Dropout(x)
# Bottleneck线性层将特征映射到长度为embedding_size的特征空间
x = self.Bottleneck(x)
# BatchNorm1d层对特征向量进行归一化处理,并返回未标准化前的结果
before_normalize = self.last_bn(x)
# L2标准化使特征向量具有单位长度
x = F.normalize(before_normalize, p=2, dim=1)
return before_normalize, x
def forward_classifier(self, x):
# 输入特征向量经过分类器线性层进行分类预测
x = self.classifier(x)
return x
这段代码承接了nets/mobilenet.py的输出,并对其进行处理。
输入数据经过backbone网络(如MobileNetV1或Inception-ResNetv1)进行特征提取。这些backbone网络被设计用于从输入图像中学习有意义的高级特征表示。
经过backbone网络后,得到一个包含空间信息的特征图。接下来,应用自适应平均池化层(avg)将特征图转换为大小为1x1的全局特征。
将全局特征展平成一维向量,以便进一步处理。然后,通过Dropout层对特征向量进行随机丢弃,以减少过拟合风险。
特征向量通过Bottleneck线性层(Bottleneck)映射到长度为128的特征空间。这个线性层可以看作是一个嵌入层,它将高维特征映射到低维度的特征向量。
最后,特征向量通过BatchNorm1d层(last_bn)进行归一化处理。BatchNorm1d层对特征向量的每个元素进行标准化,使其具有零均值和单位方差,从而增加模型的稳定性和泛化能力。
通过根据初步特征图提取长度为128的特征向量,我们可以获得更具有判别能力和泛化能力的人脸特征表示。这种特征表示在人脸识别、验证和分类等任务中具有重要意义,并且可以应用于各种实际场景中。
总而言之,应用L2标准化处理特征向量的目的是消除尺度差异、提高相似度度量以及减少冗余信息。这样做有助于改善模型的性能和鲁棒性,并为后续的人脸识别任务提供更加一致和可靠的特征表示。
在训练模式下,Facenet模型中的分类器(classifier)可以用于具体的人脸分类任务,并且对辅助Triplet Loss的收敛起到重要作用。以下是详细介绍:
初始化方法(init):当mode参数为"train"时,在初始化过程中创建了一个额外的线性层(classifier)用于人脸分类。
参数num_classes表示分类任务中的类别数量。
前向传播方法(forward_classifier):该方法仅对特征向量进行分类预测。
输入特征向量经过分类器线性层进行分类预测。
输出结果是每个类别的得分或概率。
总结起来,在训练模式下使用分类器进行具体的人脸分类,既可以辅助Triplet Loss的收敛,提高模型的人脸特征表达能力,也可以为人脸分类任务提供准确的预测结果。这有助于改善模型的性能和鲁棒性,并且使其适用于各种实际应用场景。
CASIA-WebFace是一个用于人脸识别研究的大规模数据集。该数据集由中国科学院自动化研究所(CASIA)创建,并于2014年发布。
CASIA-WebFace数据集包含了来自互联网上的10,575个身份的494,414张人脸图像。这些图像涵盖了不同种族、性别和年龄段的人脸,具有较高的多样性。
每个身份在数据集中都有至少一张正面人脸图像,而某些身份可能会有多张图像。这使得CASIA-WebFace成为一个适合进行人脸识别算法研究和评估的丰富资源。
CASIA-WebFace数据集的图像分辨率较高,通常为250x250像素。此外,数据集还提供了标注信息,包括每个图像对应的身份标签。
由于CASIA-WebFace数据集的规模和多样性,它已经成为许多人脸识别算法研究的基准数据集之一。研究人员可以使用该数据集来开发和测试人脸检测、人脸对齐、特征提取和人脸识别等相关技术。
需要注意的是,CASIA-WebFace数据集仅供非商业目的使用,需要遵守相关的数据使用规定和法律法规。
总之,CASIA-WebFace是一个用于人脸识别研究的大规模数据集,包含了来自互联网上的10,575个身份的494,414张人脸图像。该数据集具有高分辨率、多样性和标注信息,适合进行人脸识别算法的开发和评估
facenet使用Triplet Loss作为loss。
Triplet Loss的输入是一个三元组
a:anchor,基准图片获得的128维人脸特征向量
p:positive,与基准图片属于同一张人脸的图片获得的128维人脸特征向量
n:negative,与基准图片不属于同一张人脸的图片获得的128维人脸特征向量
我们可以将anchor和positive求欧几里得距离,并使其尽量小。
我们可以将anchor和negative求欧几里得距离,并使其尽量大。
当我们使用Facenet进行人脸识别时,我们希望模型能够学会将同一个人的不同照片特征聚集在一起,并且将不同人的特征分开。为了实现这个目标,我们使用了Triplet Loss作为损失函数。
Triplet Loss的思想是比较三张图片之间的相似性:
基准图片(anchor)、同一个人的另一张图片(positive)和不同人的图片(negative)。
我们希望基准图片与同一个人的图片之间的距离尽量小,而与不同人的图片之间的距离尽量大。
通过计算欧几里得距离来衡量图片之间的距离,然后使用公式L=max(d(a,p)−d(a,n)+margin,0)来计算损失值。
其中,d(a,p)表示基准图片与同一个人的图片之间的距离,d(a,n)表示基准图片与不同人的图片之间的距离,margin是一个常数。
我们希望让同一个人的不同状态下的特征更加接近,而不同人的特征则更加远离。通过优化这个损失函数,我们可以使模型更好地理解人脸特征,从而提高人脸识别的准确性。
但是仅使用Triplet Loss可能导致模型难以收敛,所以我们还结合了Cross-Entropy Loss来辅助训练。Cross-Entropy Loss用于人脸分类任务,帮助模型更好地区分不同的人脸。
通过综合使用这两种损失函数,可以提高模型在人脸识别中的性能和稳定性。