ModelArts-Lab 第十四期:学习心得及拓展——人脸检测MTCNN和人脸识别Facenet 原理和实践
本文心得来源学习【ModelArts-Lab AI实战营】第十四期:人脸识别案例 ,通过该次学习,了解人脸检测MTCNN和人脸识别Facenet 原理并进行了实践。
首先我们来看看人脸识别的定义:
用摄像机或摄像头采集含有人脸的图像或视频流,并自动在图像中检测和跟踪人脸,进而对检测到的人脸进行脸部识别的一系列相关技术,通常也叫做人像识别、面部识别。
我们在实践中使用了两大算法:一个是人脸检测;另一个是人脸识别。
人脸检测用的是 MTCNN 算法
人脸识别用的是 FaceNet 算法
两者都是深度学习算法,在图像领域深度学习算法已经远远领先于传统的机器学习算法,而且视觉领域工业界的应用也是深度学习算法居多。
一、预备知识
在学习人脸检测前需要了解的理论知识:
IoU
IoU 的全称为交并比(Intersection-over-Union,IoU),目标检测中使用的一个概念,通过这个名称我们大概可以猜到 IoU 的计算方法。
,是产生的候选框(candidate bound)与原标记框(ground truth bound)的交叠率,即它们的交集与并集的比值。最理想情况是完全重叠,即比值为1。
NMS
非极大值抑制,它就是一个寻找局部最大值的过程。
在进行目标检测时一般会采取窗口滑动的方式,同一个人可能有好几个框(每一个框都带有一个分类器得分)
而我们的目标是一个人只保留一个最优的框:
于是我们就要用到非极大值抑制,来抑制那些冗余的框: 抑制的过程是一个迭代-遍历-消除的过程。
(1)将所有框的得分排序,选中最高分及其对应的框:
(2)遍历其余的框,如果和当前最高分框的重叠面积(IOU)大于一定阈值,我们就将框删除。
比如下面这张图,一个人脸生成了 3 个框,红色框是得分最高的:
我们需要将红色的框拿出来,然后用剩下的绿框分别跟它计算 IoU,只要 IoU 大于一定阈值(一般为 0.7),那么我们就将其删掉,最终只剩下一个红色的框。
(3)从未处理的框中继续选一个得分最高的,重复上述过程。
一般会得出一个得分(score),比如人脸检测,会在很多框上都有得分,然后把这些得分全部排序。选取得分最高的那个框,接下来计算其他的框与当前框的重合程度(IoU),如果重合程度大于一定阈值就删除,因为在同一个脸上可能会有好几个高得分的框,都是人脸但是不需要那么框我们只需要一个就够了。
二、MTCNN原理
(一)MTCNN 出现的背景
MTCNN(Multi-task convolutional neural network) 出现之前,人脸检测主要有传统方法 DPM 和深度学习方法 Faceness、CascadeCNN,但是这些方法在工业级的应用上并不是特别理想。由于人脸检测效果一般,致使建立在检测基础之上的人脸对齐效果也不是很突出。
在这个大背景下,MTCNN 应运而生,发表在 2016 年 ECCV 上。MTCNN将人脸区域检测与人脸关键点检测放在了一起,它的主题框架类似于cascade。总体可分为P-Net、R-Net、和O-Net三层网络结构。
架构亮点
图像金字塔
在将图像放入模型之前,将图像按照不同尺度缩小成 n 个图像,然后将 n 个不同大小的图像同时放入网络进行训练。可以说跟多尺度训练有异曲同工之妙。在不扩增数据集的基础上,变相的做了一个数据增强操作。
减小卷积核大小,增加模型深度
相比与其他多分类物体检测和分类任务,人脸检测是二分类问题,不需要更大的卷积核而是需要更小的卷积核。因此,将卷积核由 5*5 变为 3*3 并增加深度。这样在减小模型大小的基础上还提高了模型精度。
(二)MTCNN 算法架构
1、数据准备
在标注好的人脸图片上进行随机切割,根据切割到的边框和真实的人脸框进行 IoU 计算,将 IoU > 0.65 的划分为 Positive 正样本数据,将 IoU < 0.3 的划分为 Negative 负样本数据,将 IoU 在 0.4-0.65 之间的划分为 Part 数据。
将标注好的 5 个特征点划分为 Landmark face 数据。
这四种数据之间的比例为:
negative/positive/part faces/landmark = 3:1:1:2
这四种数据的用途:
negative 和 positive 用于人脸分类
positive 和 part faces 用于边框回归
landmark face 用于人脸特征点定位(人脸对齐/关键点检测)
2、模型结构
整个 MTCNN 有三个不同的 CNN 层组成。
(1)Proposal Net结构图如下:
它的输入数据为:所有训练样本 resize 为 shape=(12x12x3) 的图像,输出的结果有 3 种:
face classification:输入图像为人脸图像的概率
bounding box:输出矩形框位置信息
facial landmark localization:输入人脸样本的 5 个关键点位置
(2)Refinement Net
结构图如下:
它的输入数据为:所有训练样本 resize 为 shape=(24x24x3) 的图像。输出结果跟 PNet 输出结果类型一样, 也是那 3 种。
(3)Output Net
结构图如下:
它的输入数据为:所有训练样本 resize 为 shape=(48x48x3) 的图像。输出结果跟以上两个结构输出结果类型一样,也是那 3 种。
Summary
从 PNet 到 RNet 再到 ONet,网络的输入图像尺寸越来越大,结构越来越深,提取的特征也越具有表现能力。这体现了 coarse-to-fine 的策略:
第一步通过浅层的 CNN 选出候选窗口;
第二步用更复杂的 CNN 过滤掉没有脸部的窗口;
第三步用更强大的 CNN 调优结果。
(三)损失函数
MTCNN 损失有三个:
人脸分类损失
边框定位回归损失
特征点定位回归损失
1、人脸分类损失
一个二分类的交叉熵损失函数:
其中
输入:x
预测:p
groundtruth label:y
2、边框定位回归损失
其中
y’:预测值边框
y:真值框(groundtrue box)
y 值为 (y,x,h,w),其中 y,x 为左上角坐标
3、特征点定位回归损失
其中
y’:预测值
y:真值
包括:左眼、右眼、鼻子、左嘴角、右嘴角 5 个点。$y∈R^{10}$
注意:一般的损失函数之所以用差的平方,主要是通过这种处理可以得到正的损失值,如果有正有负的话两者会相互抵消。由于绝对值不适合求导,所以一般不用它当做损失函数。在人脸算法中和一般的回归算法中,欧几里得损失函数比较常用。
多源训练
在不同的 CNN 层,对不同的损失函数, 比重不同。在 PNet 和 RNet 中, 主要任务是将是不是人脸给区分开来,所以 人脸分类损失 占的比重大;在 ONet 中, 主要目标是将人脸的关键点找出来,所以特征点定位回归损失占的比重稍微大点。
训练技巧:OHSM(Online Hard Example Mining)
在前向传播中,对于全部的样本,根据 Loss 进行由大到小进行排序,选择前 70%。
在反向传播中,只对这前 70% 的样本进行反向传播,这样,忽略了对检测更有帮助的 easy example,加强了对检测更有帮助的 hard example 的惩罚。
训练流程
准备数据,包括人脸检测数据和关键点检测数据
数据生成,通过随机裁剪在数据集上进行剪裁, 然后保存成 12*12 大小的图片,根据 IoU 的不同,划分成 pos、neg、part、landmark
用生成的 4 种数据训练 PNet 网络
用训练好的 PNet 模型生成难训练的人脸检测数据样本,图片大小是 24*24
生成 24*24 大小的 landmark 数据
训练 RNet 网络
用训练好的 PNet 模型生成难训练的人脸检测数据样本,图片大小是 48*48
生成 48*48 大小的 landmark 数据
训练 ONet 网络
也就说整个 MTCNN 需要依次训练 3 个模型结构。
三、faceNet原理
FaceNet Github地址: https://github.com/davidsandberg/facenet
参考资料:https://blog.csdn.net/fire_light_/article/details/79592804
Google工程师Florian Schroff,Dmitry Kalenichenko,James Philbin提出了人脸识别FaceNet模型,该模型没有用传统的softmax的方式去进行分类学习,而是抽取其中某一层作为特征,学习一个从图像到欧式空间的编码方法,然后基于这个编码再做人脸识别、人脸验证和人脸聚类等。
FaceNet主要用于验证人脸是否为同一个人,通过人脸识别这个人是谁。FaceNet的主要思想是把人脸图像映射到一个多维空间,通过空间距离表示人脸的相似度。同个人脸图像的空间距离比较小,不同人脸图像的空间距离比较大。这样通过人脸图像的空间映射就可以实现人脸识别,FaceNet中采用基于深度神经网络的图像映射方法和基于triplets(三联子)的loss函数训练神经网络,网络直接输出为128维度的向量空间。
FaceNet的网络结构如下图所示,其中Batch表示人脸的训练数据,接下来是深度卷积神经网络,然后采用L2归一化操作,得到人脸图像的特征表示,最后为三元组(Triplet Loss)的损失函数。
四、人脸识别系统实践
下面,我们实现一个人脸检测系统来预测给定人脸的身份。
我们将使用 MTCNN 模型进行人脸检测,使用 FaceNet 模型为每个检测到的人脸创建人脸embedding,然后使用一个线性支持向量机(Linear Support Vector Machine (SVM),SVM)分类器模型来预测给定人脸的身份。
既然是人脸识别,肯定要有已知人脸的数据库,不想去网上扒拉数据集了,我们自己做一个吧,比如我们
公司新到了两位员工,分别是巩同学和章同学,
把两位大神的人像的收集到 \facenet-ty\face-img\ 文件夹下:
其中\tain目录下收集的是训练集,每新增一个人需新建一个文件夹,巩同学和章同学各有10张单人照片分别放到“gongli”、“zhangziyi”目录下;因为仅用于测试,我们的人脸数据库只有两人的照片,如需新增图库,只需在train目录下继续新建文件夹,然后将单人照片放在里面即可,图片名称可以是任意,注意制作人脸数据库时,必须使用单人照片;
\val 目录下收集的是验证集,建立同样的目录文件,两人各有6张图片;
这些照片提供了各种方向、光照和各种大小的人脸。重要的是,每张照片都包含一张人脸。
我们使用这个数据集作为分类器的基础,对train数据集进行训练,并对val数据集中的人脸进行分类。
1、检测人脸
我们检测数据集中每张照片的人脸,并将数据集缩减为一系列人脸,
首先我们需要创建一个人脸检测器,在\facenet-ty\face-img\ 目录下,我们可以试着检测一下训练集中gongli的10张照片,并创建包含10张人脸的图像,图像有两行,每行5张人脸,具体代码如下:
from os import listdir
from PIL import Image
from numpy import asarray
from matplotlib import pyplot
from mtcnn.mtcnn import MTCNN
#我们创建一个函数 extract_face() ,它将从加载的文件名加载照片,并将提取的人脸调整为输入的尺寸,
#它假定每张照片包含一张人脸,并将返回检测到的第一张人脸。
def extract_face(filename,required_size=(160,160)):
#第一步是以 NumPy 数组的形式加载图像,我们可以使用 PIL 库和 open() 函数来实现。
#并将图像转换为 RGB,以防图像出现 alpha 通道或变成黑白。
image=Image.open(filename)
image=image.convert('RGB')
pixels = asarray(image)
#接下来,我们可以创建一个 MTCNN 人脸检测器类detector ,并使用它来检测加载的照片中所有的人脸。
detector = MTCNN()
results = detector.detect_faces(pixels)
#结果是一个边界框列表,其中每个边界框定义了边界框的左下角,以及宽度和高度。
#如果我们假设照片中只有一张人脸用于实验,我们可以确定边界框的像素坐标如下。
#有时候库会返回负像素索引,可以通过取坐标的绝对值来解决这一问题。
# extract the bounding box from the first face
x1, y1, width, height = results[0]['box']
x1, y1 = abs(x1), abs(y1)
x2, y2 = x1 + width, y1 + height
#我们可以使用这些坐标来提取人脸。
face = pixels[y1:y2, x1:x2]
# resize pixels to the model size
#然后我们可以使用 PIL 库将这个人脸的小图像调整为所需的尺寸;
#具体而言,模型需要形状为 160x160 的正方形输入。
image = Image.fromarray(face)
image = image.resize((160, 160))
face_array = asarray(image)
return face_array
folder='./face-img/train/gongli/'
i=1
for filename in listdir(folder):
path=folder+filename
face=extract_face(path)
print(i,face.shape)
pyplot.subplot(2,5,i)
pyplot.axis('off')
pyplot.imshow(face)
i += 1
pyplot.show()
接下来,我们扩展这个示例代码,遍历给定数据集的每个子目录(例如“train”或“val”),提取出人脸,并为每个检测到的人脸准备一个以名称作为输出标签的数据集。
下面的 load_dataset() 函数输入目录名称,如“face-img/train/”,为每个子目录(人名)检测人脸,为每个检测到的人脸分配标签,标签可以从目录名中提取。
def load_faces(directory):
faces=list()
for filename in listdir(directory):
path = directory+filename
print(path)
face = extract_face(path)
faces.append(face)
return faces
def load_dataset(directory):
x,y = list(),list()
for subdir in listdir(directory):
path=directory+subdir+'/'
if not isdir(path):
continue
print(path)
faces = load_faces(path)
# create labels
labels = [subdir for _ in range(len(faces))]
# summarize progress
print('>loaded %d examples for class: %s' % (len(faces), subdir))
# store
x.extend(faces)
y.extend(labels)
return asarray(x), asarray(y)
trainX, trainy = load_dataset('face-img/train/')
我们通过加载“train”数据集中的所有照片,然后提取人脸,得到 20 个样本,其中正方形人脸输入和类标签字符串作为输出。
然后加载“val ” 数据集,提供 12 个可用作测试数据集的样本。
testX, testy = load_dataset('face-img/val/')
最后将这两个数据集保存到“2-faces-dataset.npz”的压缩 NymPy 数组文件,存放在当前的工作目录中。
savez_compressed('2-faces-dataset.npz', trainX, trainy, testX, testy)
数据准备就绪后,就可提供给人脸检测模型。
2、创建人脸embedding
人脸embedding是一个向量,表示从人脸中提取的特征。并可以将其与为其他人脸生成的向量进行比较,
通过某种标准,如果一个向量与另一个向量距离较近,很可能是同一个人,而距离较远的较量则可能是不同的人。
我们要开发的分类器模型将人脸embedding作为输入,并预测该人脸的身份。FaceNet 模型将为给定的人脸图像生成此embedding。
FaceNet 模型可以用作分类器本身的一部分,或者,我们可以用 FaceNet 模型对人脸进行预处理,创建可以存储并用作分类器模型输入的人脸embedding。因为 FaceNet 模型既大又慢,不利于创建人脸embedding。考虑到速度问题,本次实践我们选择后一种方法,
首先我们可以预先计算训练中所有人脸的人脸embedding。
import numpy as np
from numpy import load
from numpy import expand_dims
from numpy import asarray
from numpy import savez_compressed
from keras.models import load_model
def get_embedding(model, face_pixels):
# scale pixel values
face_pixels = face_pixels.astype('float32')
# standardize pixel values across channels (global) 对图像像素进行标准化,以满足 FaceNet 模型的要求。
mean, std = face_pixels.mean(), face_pixels.std()
face_pixels = (face_pixels - mean) / std
#为了对 Keras 中的每一个样本进行预测,我们必须扩展维数,使人脸数组成为一个样本。
# transform face into one sample
#print(face_pixels.shape) # 本例中为 (160, 160, 3)
samples = expand_dims(face_pixels, axis=0)
# print(samples.shape) # 本例中为 (1, 160, 160, 3)
# make prediction to get embedding 然后,利用该模型进行预测,提取嵌入向量。
yhat = model.predict(samples)
return yhat[0]
# load the face dataset
data = np.load('2-faces-dataset.npz')
trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']
print('Loaded: ', trainX.shape, trainy.shape, testX.shape, testy.shape)
#Loaded: (20, 160, 160, 3) (20,) (12, 160, 160, 3) (12,)
# load the facenet model
model = load_model('facenet_keras.h5')
print('Loaded Model')
# convert each face in the train set to an embedding
newTrainX = list()
for face_pixels in trainX:
embedding = get_embedding(model, face_pixels)
newTrainX.append(embedding)
newTrainX = asarray(newTrainX)
print(newTrainX.shape)
# convert each face in the test set to an embedding
newTestX = list()
for face_pixels in testX:
embedding = get_embedding(model, face_pixels)
newTestX.append(embedding)
newTestX = asarray(newTestX)
print(newTestX.shape)
# save arrays to one file in compressed format
savez_compressed('2-faces-embeddings.npz', newTrainX, trainy, newTestX, testy)
我们将训练数据集转换成 20 个人脸embeddings,每个embeddings由 128 个元素向量组成。验证数据集中的 12 个样本也被转换为了人脸embeddings。
然后将得到的数据集保存到压缩的 NumPy 数组中,存在当前的工作目录中,名为“2-faces-embeddings.npz”。
3、执行人脸分类
首先,加载人脸嵌入数据集
from numpy import load
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import Normalizer
from sklearn.svm import SVC
from matplotlib import pyplot
#load faces
data = load('2-faces-dataset.npz')
#trainX, trainy, testX, testy 取的是testX的faces数据
testX_faces = data['arr_2']
len(testX_faces)
# load face embeddings
data = load('2-faces-embeddings.npz')
trainX, trainy, testX, testy = data['arr_0'], data['arr_1'], data['arr_2'], data['arr_3']
print('Dataset: train=%d, test=%d' % (trainX.shape[0], testX.shape[0]))
#对人脸embeddings向量进行归一化处理
# normalize input vectors
in_encoder = Normalizer(norm='l2')
trainX = in_encoder.transform(trainX)
testX = in_encoder.transform(testX)
#接下来,需要将每个名人姓名的字符串目标变量转换为整数。
#这可以通过 scikit-learn 中的 LabelEncoder 类 来实现。
# label encode targets
out_encoder = LabelEncoder()
out_encoder.fit(trainy)
trainy = out_encoder.transform(trainy)
testy = out_encoder.transform(testy)
接下来,我们可以拟合一个模型。
在处理归一化人脸嵌入输入时,通常使用 线性支持向量机(SVM)。这是因为该方法在分离人脸嵌入向量方面非常有效。我们可以使用 scilit-learn 中的 SVC 类将线性 SVM 拟合到训练数据中,并将“kernel”属性设置为“linear”。我们可能还希望在以后进行预测时使用概率,可以通过将“probability”设置为“True”。
# fit model
model = SVC(kernel='linear',probability=True)
model.fit(trainX, trainy)
接下来,我们就可以对模型进行评估了。
通过使用拟合模型对训练和测试数据集中的每个样本进行预测,然后计算分类正确率。
# predict
yhat_train = model.predict(trainX)
yhat_test = model.predict(testX)
# score
score_train = accuracy_score(trainy, yhat_train)
score_test = accuracy_score(testy, yhat_test)
# summarize
print('Accuracy: train=%.3f, test=%.3f' % (score_train*100, score_test*100))
4、模型预测
我们可以通过显示原始人脸和预测,让它变得更有趣。
首先,加载人脸数据集,特别是测试数据集中的人脸。
从测试集中随机选择一个样本,然后获取嵌入、人脸像素、期望的类预测以及类的相应名称。
from random import choice
# test model on a random example from the test dataset
selection = choice([i for i in range(testX.shape[0])])
random_face_pixels = testX_faces[selection]
random_face_emb=testX[selection]
random_face_class = testy[selection]
random_face_name = out_encoder.inverse_transform([random_face_class])
#进行维度转换
samples = expand_dims(random_face_emb,axis=0)
yhat_class = model.predict(samples)
yhat_prob = model.predict_proba(samples)
#get name
class_index = yhat_class[0]
class_probability = yhat_prob[0,class_index]*100
predict_names = out_encoder.inverse_transform(yhat_class)
print('Predicted:%s (%.3f)' % (predict_names[0],class_probability))
print('Expected: %s' % random_face_name[0])
#绘制结果
pyplot.imshow(random_face_pixels)
title='%s (%.3f)' % (predict_names[0],class_probability)
pyplot.title(title)
pyplot.show()
每次运行时,都会从测试数据集中选择一个不同的随机样本进行预测。
在例中,选择了章同学的一张照片,并在绘制的图像的标题中正确地显示了预测的名称和概率。
- 点赞
- 收藏
- 关注作者
评论(0)