学习前言
好的还有Pytorch版。
什么是Facenet
谷歌人脸识别算法,发表于 CVPR 2015,利用相同人脸在不同角度等姿态的照片下有高内聚性,不同人脸有低耦合性,提出使用 cnn + triplet mining 方法,在 LFW 数据集上准确度达到 99.63%。
通过 CNN 将人脸映射到欧式空间的特征向量上,实质上:不同图片人脸特征的距离较大;通过相同个体的人脸的距离,总是小于不同个体的人脸这一先验知识训练网络。
测试时只需要计算人脸特征EMBEDDING,然后计算距离使用阈值即可判定两张人脸照片是否属于相同的个体。
简单来讲,在使用阶段,facenet即是:
1、输入一张人脸图片
2、通过深度卷积网络提取特征
3、L2标准化
4、得到一个长度为128特征向量。
源码下载
https://github.com/bubbliiiing/facenet-pytorch
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的卷积进行通道处理,
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.AdaptiveAvgPool2d((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
2、根据初步特征获得长度为128的特征向量
利用主干特征提取网络我们可以获得一个特征层,它的shape为(batch_size, h, w, channels),我们可以将其取全局平均池化,方便后续的处理(batch_size, channels)。
我们可以将平铺后的特征层进行一个神经元个数为128的全连接。此时我们相当于利用了一个长度为128的特征向量代替输入进来的图片。这个长度为128的特征向量就是输入图片的特征浓缩。
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.AdaptiveAvgPool2d((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
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} ∣∣x∣∣2=∑i=1Nxi2,也就是欧几里得范数,即向量元素绝对值的平方和再开方。
L2标准化就是每个元素/L2范数;
在pytorch代码中,只需要一行就可以实现l2标准化的层。
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.AdaptiveAvgPool2d((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
到这里,我们输入进来的图片,已经变成了一个经过l2标准化的长度为128的特征向量了!
4、构建分类器(用于辅助Triplet Loss的收敛)
当我们完成第三步后,我们已经可以利用这个预测结果进行训练和预测了。
但是由于仅仅只是用Triplet Loss会使得整个网络难以收敛,本文结合Cross-Entropy Loss和Triplet Loss作为总体loss。
Triplet Loss用于进行不同人的人脸特征向量欧几里得距离的扩张,同一个人的不同状态的人脸特征向量欧几里得距离的缩小。
Cross-Entropy Loss用于人脸分类,具体作用是辅助Triplet Loss收敛。
想要利用Cross-Entropy Loss进行训练需要构建分类器,因此对第三步获得的结果再次进行一个全连接用于分类。
构建代码如下,当我们在进行网络的训练的时候,可使用分类器辅助训练,在预测的时候,分类器是不需要的:
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.AdaptiveAvgPool2d((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
二、训练部分
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.pth或者facenet_mobilenet.pth放在model_data文件夹内。
在train.py中指定合适的模型预训练权重路径。facenet_inception_resnetv1.pth是我已经训练过的基于inception_resnetv1的facenet网络;
facenet_mobilenet.pth是我已经训练过的基于mobilenet的facenet网络。
运行train.py开始训练。
转载:https://blog.csdn.net/weixin_44791964/article/details/108220265