(转)第三十七节、人脸检测MTCNN和人脸识别Facenet(附源码)

http://www.cnblogs.com/zyly/p/9703614.html

在说到人脸检测我们首先会想到利用Harr特征提取和Adaboost分类器进行人脸检测(有兴趣的可以去一看这篇博客第九节、人脸检测之Haar分类器),其检测效果也是不错的,但是目前人脸检测的应用场景逐渐从室内演变到室外,从单一限定场景发展到广场、车站、地铁口等场景,人脸检测面临的要求越来越高,比如:人脸尺度多变、数量冗大、姿势多样包括俯拍人脸、戴帽子口罩等的遮挡、表情夸张、化妆伪装、光照条件恶劣、分辨率低甚至连肉眼都较难区分等。在这样复杂的环境下基于Haar特征的人脸检测表现的不尽人意。随着深度学习的发展,基于深度学习的人脸检测技术取得了巨大的成功,在这一节我们将会介绍MTCNN算法,它是基于卷积神经网络的一种高精度的实时人脸检测和对齐技术。

搭建人脸识别系统的第一步就是人脸检测,也就是在图片中找到人脸的位置。在这个过程中输入的是一张含有人脸的图像,输出的是所有人脸的矩形框。一般来说,人脸检测应该能够检测出图像中的所有人脸,不能有漏检,更不能有错检。

获得人脸之后,第二步我们要做的工作就是人脸对齐,由于原始图像中的人脸可能存在姿态、位置上的差异,为了之后的统一处理,我们要把人脸“摆正”。为此,需要检测人脸中的关键点,比如眼睛的位置、鼻子的位置、嘴巴的位置、脸的轮廓点等。根据这些关键点可以使用仿射变换将人脸统一校准,以消除姿势不同带来的误差。

一 MTCNN算法结构

MTCNN算法是一种基于深度学习的人脸检测和人脸对齐方法,它可以同时完成人脸检测和人脸对齐的任务,相比于传统的算法,它的性能更好,检测速度更快。

MTCNN算法包含三个子网络:Proposal Network(P-Net)、Refine Network(R-Net)、Output Network(O-Net),这三个网络对人脸的处理依次从粗到细。

在使用这三个子网络之前,需要使用图像金字塔将原始图像缩放到不同的尺度,然后将不同尺度的图像送入这三个子网络中进行训练,目的是为了可以检测到不同大小的人脸,从而实现多尺度目标检测。

1、P-Net网络

P-Net的主要目的是为了生成一些候选框,我们通过使用P-Net网络,对图像金字塔图像上不同尺度下的图像的每一个12×1212×12区域都做一个人脸检测(实际上在使用卷积网络实现时,一般会把一张h×wh×w的图像送入P-Net中,最终得到的特征图每一点都对应着一个大小为12×1212×12的感受野,但是并没有遍历全一张图像每一个12×1212×12的图像)。

P-Net的输入是一个12×12×312×12×3的RGB图像,在训练的时候,该网络要判断这个12×1212×12的图像中是否存在人脸,并且给出人脸框的回归和人脸关键点定位;

在测试的时候输出只有NN个边界框的4个坐标信息和score,当然这4个坐标信息已经使用网络的人脸框回归进行校正过了,score可以看做是分类的输出(即人脸的概率):

  • 网络的第一部分输出是用来判断该图像是否包含人脸,输出向量大小为1×1×21×1×2,也就是两个值,即图像是人脸的概率和图像不是人脸的概率。这两个值加起来严格等于1,之所以使用两个值来表示,是为了方便定义交叉熵损失函数。
  • 网络的第二部分给出框的精确位置,一般称为框回归。P-Net输入的12×1212×12的图像块可能并不是完美的人脸框的位置,如有的时候人脸并不正好为方形,有可能12×1212×12的图像偏左或偏右,因此需要输出当前框位置相对完美的人脸框位置的偏移。这个偏移大小为1×1×41×1×4,即表示框左上角的横坐标的相对偏移,框左上角的纵坐标的相对偏移、框的宽度的误差、框的高度的误差。
  • 网络的第三部分给出人脸的5个关键点的位置。5个关键点分别对应着左眼的位置、右眼的位置、鼻子的位置、左嘴巴的位置、右嘴巴的位置。每个关键点需要两维来表示,因此输出是向量大小为1×1×101×1×10。

2、R-Net

由于P-Net的检测时比较粗略的,所以接下来使用R-Net进一步优化。R-Net和P-Net类似,不过这一步的输入是前面P-Net生成的边界框,不管实际边界框的大小,在输入R-Net之前,都需要缩放到24×24×324×24×3。网络的输出和P-Net是一样的。这一步的目的主要是为了去除大量的非人脸框。

3、O-Net

进一步将R-Net的所得到的区域缩放到48×48×348×48×3,输入到最后的O-Net,O-Net的结构与P-Net类似,只不过在测试输出的时候多了关键点位置的输出。输入大小为48×48×348×48×3的图像,输出包含PP个边界框的坐标信息,score以及关键点位置。

 从P-Net到R-Net,再到最后的O-Net,网络输入的图像越来越大,卷积层的通道数越来越多,网络的深度也越来越深,因此识别人脸的准确率应该也是越来越高的。同时P-Net网络的运行速度越快,R-Net次之、O-Net运行速度最慢。之所以使用三个网络,是因为一开始如果直接对图像使用O-Net网络,速度会非常慢。实际上P-Net先做了一层过滤,将过滤后的结果再交给R-Net进行过滤,最后将过滤后的结果交给效果最好但是速度最慢的O-Net进行识别。这样在每一步都提前减少了需要判别的数量,有效地降低了计算的时间。

二 MTCNN损失函数

 由于MTCNN包含三个子网络,因此其损失函数也由三部分组成。针对人脸识别问题,直接使用交叉熵代价函数,对于框回归和关键点定位,使用L2L2损失。最后把这三部分的损失各自乘以自身的权重累加起来,形成最后的总损失。在训练P-Net和R-Net的时候,我们主要关注目标框的准确度,而较少关注关键点判定的损失,因此关键点损失所占的权重较小。对于O-Net,比较关注的是关键点的位置,因此关键点损失所占的权重就会比较大。

1、人脸识别损失函数

在针对人脸识别的问题,对于输入样本xixi,我们使用交叉熵代价函数:

 

Ldeti=−(ydetilog(pi)+(1−ydeti)(1−log(pi)))Lidet=−(yidetlog(pi)+(1−yidet)(1−log(pi)))

其中ydetiyidet表示样本的真实标签,pipi表示网络输出为人脸的概率。

2、框回归

对于目标框的回归,我们采用的是欧氏距离:

 

Lboxi=∥y^boxi−yboxi∥Libox=‖y^ibox−yibox‖

其中y^boxiy^ibox表示网络输出之后校正得到的边界框的坐标,yboxiyibox是目标的真实边界框。

3、关键点损失函数

对于关键点,我们也采用的是欧氏距离:

 

Llandmarki=∥y^landmarki−ylandmarki∥Lilandmark=‖y^ilandmark−yilandmark‖

其中y^landmarkiy^ilandmark表示网络输出之后得到的关键点的坐标,ylandmarkiyilandmark是关键点的真实坐标。

 4、总损失

 把上面三个损失函数按照不同的权重联合起来:

 

min∑i=1N∑j∈{det,box,landmark}αjβjiLjimin∑i=1N∑j∈{det,box,landmark}αjβijLij

其中NN是训练样本的总数,αjαj表示各个损失所占的权重,在P-Net和R-net中,设置αdet=1,αbox=0.5,αlandmark=0.5αdet=1,αbox=0.5,αlandmark=0.5,在O-Net中,设置αdet=1,αbox=0.5,αlandmark=1αdet=1,αbox=0.5,αlandmark=1,βji∈{0,1}βij∈{0,1}表示样本类型指示符。

5、Online Hard sample mining

In particular, in each mini-batch, we sort the losses computed in the forward propagation from all samples and select the top 70% of them as hard samples. Then we only compute the gradients from these hard samples in the backward propagation.That means we ignore the easy samples that are less helpful to strengthen the detector during training. Experiments show that this strategy yields better performance without manual sampleselection.

这段话也就是说,我们在训练的时候取前向传播损失值(从大到小)前70%的样本,来进行反向传播更新参数。

6、训练数据

该算法训练数据来源于wider和celeba两个公开的数据库,wider提供人脸检测数据,在大图上标注了人脸框groundtruth的坐标信息,celeba提供了5个landmark点的数据。根据参与任务的不同,将训练数据分为四类:

  • 负样本:滑动窗口和Ground True的IOU小于0.3;
  • 正样本:滑动窗口和Ground True的IOU大于0.65;
  • 中间样本:滑动窗口和Ground True的IOU大于0.4小于0.65;
  • 关键点:包含5个关键点做标的;

上面滑动窗口指的是:通过滑动窗口或者随机采样的方法获取尺寸为12×1212×12的框:

wider数据集,数据可以从http://mmlab.ie.cuhk.edu.hk/projects/WIDERFace/地址下载。该数据集有32,203张图片,共有93,703张脸被标记,如下图所示:

celeba人脸关键点检测的训练数据,数据可从http://mmlab.ie.cuhk.edu.hk/archive/CNN_FacePoint.htm地址下载。该数据集包含5,590张 LFW数据集的图片和7,876张从网站下载的图片。

 三 人脸识别

在上面我们已经介绍了人脸检测,人脸检测是人脸相关任务的前提,人脸相关的任务主要有以下几种:

  • 人脸跟踪(视频中跟踪人脸位置变化);
  • 人脸验证(输入两张人脸,判断是否属于同一人);
  • 人脸识别(输入一张人脸,判断其属于人脸数据库记录中哪一个人);
  • 人脸聚类(输入一批人脸,将属于同一人的自动归为一类);

下面我们来详细介绍人脸识别技术:当我们通过MTCNN网络检测到人脸区域图像时,我们使用深度卷积网络,将输入的人脸图像转换为一个向量的表示,也就是所谓的特征。

那我们如何对人脸提取特征?我们先来回忆一下VGG16网络,输入的是图像,经过一系列卷积计算、全连接网络之后,输出的是类别概率。

 

在通常的图像应用中,可以去掉全连接层,使用卷积层的最后一层当做图像的“特征”,如上图中的conv5_3。但如果对人脸识别问题同样采用这样的方法,即,使用卷积层最后一层做为人脸的“向量表示”,效果其实是不好的。如何改进?我们之后再谈,这里先谈谈我们希望这种人脸的“向量表示”应该具有哪些性质。

在理想的状况下,我们希望“向量表示”之间的距离就可以直接反映人脸的相似度:

  • 对于同一个人的人脸图像,对应的向量的欧几里得距离应该比较小;

  • 对于不同人的人脸图像,对应的向量之间的欧几里得距离应该比较大;

例如:设人脸图像为x1,x2x1,x2,对应的特征为f(x1),f(x2)f(x1),f(x2),当x1,x2x1,x2对应是同一个人的人脸时,f(x1),f(x2)f(x1),f(x2)的距离∥f(x1)−f(x2)∥2‖f(x1)−f(x2)‖2应该很小,而当x1,x2x1,x2对应的不是同一个人的人脸时,f(x1),f(x2)f(x1),f(x2)的距离∥f(x1)−f(x2)∥2‖f(x1)−f(x2)‖2应该很大。

在原始的VGG16模型中,我们使用的是softmax损失,softmax是类别间的损失,对于人脸来说,每一类就是一个人。尽管使用softmax损失可以区别每个人,但其本质上没有对每一类的向量表示之间的距离做出要求。

举个例子,使用CNN对MNIST进行分类,我们设计一个特殊的卷积网络,让最后一层的向量变为2维,此时可以画出每一类对应的2维向量表示的图(图中一种颜色对应一种类别):

上图是我们直接使用softmax训练得到的结果,它就不符合我们希望特征具有的特点:

  • 我们希望同一类对应的向量表示尽可能接近。但这里同一类(如紫色),可能具有很大的类间距离;

  • 我们希望不同类对应的向量应该尽可能远。但在图中靠中心的位置,各个类别的距离都很近;

对于人脸图像同样会出现类似的情况,对此,有很改进方法。这里介绍其中两种:三元组损失函数,中心损失函数。

1、三元组损失

三元组损失函数的原理:既然目标是特征之间的距离应该具备某些性质,那么我们就围绕这个距离来设计损失。具体的,我们每次都在训练数据中抽出三张人脸图像,第一张图像标记为xaixia,第二章图像标记为xpixip,第三张图像标记为xnixin。在这样一个"三元组"中,xaixia和xpixip对应的是同一个人的图像,而xnixin是另外一个人的人脸图像。因此距离∥f(xai)−f(xpi)∥2‖f(xia)−f(xip)‖2应该很小,而距离∥f(xai)−f(xni)∥2‖f(xia)−f(xin)‖2应该很大。严格来说,三元组损失要求满足以下不等式:

 

∥f(xai)−f(xpi)∥22+α<∥f(xai)−f(xni)∥22‖f(xia)−f(xip)‖22+α<‖f(xia)−f(xin)‖22

即相同人脸间的距离平方至少要比不同人脸间的距离平方小αα(取平方主要是为了方便求导),据此,设计损失函数为:

 

Li=[∥f(xai)−f(xpi)∥22+α−∥f(xai)−f(xni)∥22]+Li=[‖f(xia)−f(xip)‖22+α−‖f(xia)−f(xin)‖22]+

这样的话,当三元组的距离满足∥f(xai)−f(xpi)∥22+α<∥f(xai)−f(xni)∥22‖f(xia)−f(xip)‖22+α<‖f(xia)−f(xin)‖22时,损失Li=0Li=0。当距离不满足上述不等式时,就会有值为∥f(xai)−f(xpi)∥22+α−∥f(xai)−f(xni)∥22‖f(xia)−f(xip)‖22+α−‖f(xia)−f(xin)‖22的损失,此外,在训练时会固定∥f(x)∥2=1‖f(x)‖2=1,以确保特征不会无限的"远离"。

三元组损失直接对距离进行优化,因此可以解决人脸的特征表示问题。但是在训练过程中,三元组的选择非常地有技巧性。如果每次都是随机选择三元组,虽然模型可以正确的收敛,但是并不能达到最好的性能。如果加入"难例挖掘",即每次都选择最难分辨率的三元组进行训练,模型又往往不能正确的收敛。对此,又提出每次都选择那些"半难"的数据进行训练,让模型在可以收敛的同时也保持良好的性能。此外,使用三元组损失训练人脸模型通常还需要非常大的人脸数据集,才能取得较好的效果。

2、中心损失

与三元组损失不同,中心损失不直接对距离进行优化,它保留了原有的分类模型,但又为每个类(在人脸模型中,一个类就对应一个人)指定了一个类别中心。同一类的图像对应的特征都应该尽量靠近自己的类别中心,不同类的类别中心尽量远离。与三元组损失函数,使用中心损失训练人脸模型不需要使用特别的采样方法,而且利用较少的图像就可以达到与单元组损失相似的效果。下面我们一起来学习中心损失的定义:

设输入的人脸图像为xixi,该人脸对应的类别是yiyi,对每个类别都规定一个类别中心,记作cyicyi。希望每个人脸图像对应的特征f(xi)f(xi)都尽可能接近中心cyicyi。因此定义损失函数为:

 

Li=12∥f(xi)−cyi∥22Li=12‖f(xi)−cyi‖22

多张图像的中心损失就是将它们的值累加:

 

Lcenter=∑iLiLcenter=∑iLi

这是一个非常简单的定义。不过还有一个问题没有解决,那就是如何确定每个类别的中心cyicyi呢?从理论上来说,类别yiyi的最佳中心应该是它对应所有图片的特征的平均值。但如果采用这样的定义,那么在每一次梯度下降时,都要对所有图片计算一次cyicyi,计算复杂度太高了。针对这种情况,不妨近似处理下,在初始阶段,先随机确定cyicyi,接着在每个batch内,使用Li=∥f(xi)−cyi∥22Li=‖f(xi)−cyi‖22对当前batch内的cyicyi也计算梯度,并使得该梯度更新cyicyi,此外,不能只使用中心损失来训练分类模型,还需要加入softmax损失,也就是说,损失最后由两部分组成,即L=Lsoftmax+λLcenterL=Lsoftmax+λLcenter,其中λλ是一个超参数。

最后来总结使用中心损失来训练人脸模型的过程。首先随机初始化各个中心cyicyi,接着不断地取出batch进行训练,在每个batch中,使用总的损失LL,除了使用神经网络模型的参数对模型进行更新外,也对cyicyi进行计算梯度,并更新中心的位置。

中心损失可以让训练处的特征具有"内聚性"。还是以MNIST的例子来说,在未加入中心损失时,训练的结果不具有内聚性。在加入中心损失后,得到的特征如下:

当中心损失的权重λλ越大时,生成的特征就会具有越明显的"内聚性"。

 四 人脸识别的实现

下面我们会介绍一个经典的人脸识别系统-谷歌人脸识别系统facenet,该网络主要包含两部分:

  • MTCNN部分:用于人脸检测和人脸对齐,输出160×160160×160大小的图像;
  • CNN部分:可以直接将人脸图像(默认输入是160×160160×160大小)映射到欧几里得空间,空间距离的长度代表了人脸图像的相似性。只要该映射空间生成、人脸识别,验证和聚类等任务就可以轻松完成;

先去GitHub下载facenet源码:https://github.com/davidsandberg/facenet,解压后如下图所示;

 

打开requirements.txt,我们可以看到我们需要安装以下依赖:

复制代码

tensorflow==1.7
scipy
scikit-learn
opencv-python
h5py
matplotlib
Pillow
requests
psutil

复制代码

后面在运行程序时,如果出现安装包兼容问题,建议这里使用pip安装,不要使用conda。

1、配置Facenet环境

将facebet文件夹加到python引入库的默认搜索路径中,将facenet文件整个复制到anaconda3安装文件目录下lib\site-packages下:

然后把剪切src目录下的文件,然后删除facenet下的所有文件,粘贴src目录下的文件到facenet下,这样做的目的是为了导入src目录下的包(这样import align.detect_face不会报错)。

在Anaconda Prompt中运行python,输入import facenet,不报错即可:

2、下载LFW数据集

接下来将会讲解如何使用已经训练好的模型在LFW(Labeled Faces in the Wild)数据库上测试,不过我还需要先来介绍一下LFW数据集。

LFW数据集是由美国马赛诸塞大学阿姆斯特分校计算机实验室整理的人脸检测数据集,是评估人脸识别算法效果的公开测试数据集。LFW数据集共有13233张jpeg格式图片,属于5749个不同的人,其中有1680人对应不止一张图片,每张图片尺寸都是250×250250×250,并且被标示出对应的人的名字。LFW数据集中每张图片命名方式为"lfw/name/name_xxx.jpg",这里"xxx"是前面补零的四位图片编号。例如,前美国总统乔治布什的第十张图片为"lfw/George_W_Bush/George_W_Bush_0010.jpg"。

数据集的下载地址为:http://vis-www.cs.umass.edu/lfw/lfw.tgz,下载完成后,解压数据集,打开打开其中一个文件夹,如下:

在lfw下新建一个文件夹raw,把lfw中所有的文件(除了raw)移到raw文件夹中。可以看到我的数据集lfw是放在datasets文件夹下,其中datasets文件夹和facenet是在同一路径下。

3、LFW数据集预处理(LFW数据库上的人脸检测和对齐)

我们需要将检测所使用的数据集校准为和训练模型所使用的数据集大小一致(160×160160×160),转换后的数据集存储在lfw_mtcnnpy_160文件夹内,

处理的第一步是使用MTCNN网络进行人脸检测和对齐,并缩放到160×160160×160。

MTCNN的实现主要在文件夹facenet/src/align中,文件夹的内容如下:

  • detect_face.py:定义了MTCNN的模型结构,由P-Net、R-Net、O-Net组成,这三个网络已经提供了预训练的模型,模型数据分别对应文件det1.npy、det2.npy、det3.npy。
  • align_dataset_matcnn.py:是使用MTCNN的模型进行人脸检测和对齐的入口代码。

使用脚本align_dataset_mtcnn.py对LFW数据库进行人脸检测和对齐的方法通过运行命令,我们打开Anaconda Prompt,来到facenet所在的路径下,运行如下命令:

python  facenet/src/align/align_dataset_mtcnn.py   datasets/lfw/raw  datasets/lfw/lfw_mtcnnpy_160 --image_size 160 --margin 32 --random_order

该命令会创建一个datasets/lfw/lfw_mtcnnpy_160的文件夹,并将所有对齐好的人脸图像存放到这个文件夹中,数据的结构和原先的datasets/lfw/raw一样。参数--image_size 160 --margin 32的含义是在MTCNN检测得到的人脸框的基础上缩小32像素(训练时使用的数据偏大),并缩放到160×160160×160大小,因此最后得到的对齐后的图像都是160×160160×160像素的,这样的话,就成功地从原始图像中检测并对齐了人脸。

下面我们来简略的分析一下align_dataset_mtcnn.py源文件,先上源代码如下,然后我们来解读一下main()函数

 View Code

  • 首先加载LFW数据集;
  • 建立MTCNN网络,并预训练(即使用训练好的网络初始化参数),Google Facenet的作者在建立网络时,自己重写了CNN网络所需的各个组件,包括conv层,MaxPool层,Softmax层等等,由于作者写的比较复杂。有兴趣的同学看看MTCNN 的 TensorFlow 实现这篇博客,博主使用Keras重新实现了MTCNN网络,也比较好懂代码链接:https://github.com/FortiLeiZhang/model_zoo/tree/master/TensorFlow/mtcnn;
  • 调用align.detect_face.detect_face()函数进行人脸检测,返回校准后的人脸边界框的位置、score、以及关键点坐标;
  • 对人脸框进行处理,从原图中裁切(先进行了边缘扩展32个像素)、以及缩放(缩放到160×160160×160)等,并保存相关信息到文件;

关于人脸检测的具体细节可以查看detect_face()函数,代码也比较长,这里我放上代码,具体细节部分可以参考MTCNN 的 TensorFlow 实现这篇博客。

 View Code

 4、使用已有模型验证LFW数据集准确率

项目的原作者提供了两个预训练的模型,分别是基于CASIA-WebFace和VGGFace2人脸库训练的,下载地址:https://github.com/davidsandberg/facenet:

注意:这两个模型文件需要FQ才能够下载!!!!!!

这里我们使用的预训练模型是基于数据集CASIA-WebFace的,并且使用的卷积网络结构是Inception ResNet v1,训练好的模型在LFW上可以达到99.05%左右的准确率。下载好模型后,将文件解压到facenet/models文件夹下(models文件夹需要自己新建)。解压后,会得到一个20180408-102900的文件夹,里面包含四个文件:

  • model.meta:模型文件,该文件保存了metagraph信息,即计算图的结构;
  • model.ckpt.data:权重文件,该文件保存了graph中所有遍历的数据;
  • model.ckpt.index:该文件保存了如何将meta和data匹配起来的信息;
  • pb文件:将模型文件和权重文件整合合并为一个文件,主要用途是便于发布,详细内容可以参考博客https://blog.csdn.net/yjl9122/article/details/78341689;
  • 一般情况下还会有个checkpoint文件,用于保存文件的绝对路径,告诉TF最新的检查点文件(也就是上图中后三个文件)是哪个,保存在哪里,在使用tf.train.latest_checkpoint加载的时候要用到这个信息,但是如果改变或者删除了文件中保存的路径,那么加载的时候会出错,找不到文件;

到这里、我们的准备工作已经基本完成,测试数据集LFW,模型、程序都有了,我们接下来开始评估模型的准确率。

我们打开Anaconda Prompt,来到facenet路径下(注意这里是facenet路径下),运行如下命令:

python src/validate_on_lfw.py  ../datasets/lfw/lfw_mtcnnpy_160 models/20180408-102900

运行结果如下:

由此,我们验证了模型在LFW上的准确率为99.7%。

validate_on_lfw.py源码如下:

 View Code

  • 首先加载data/pairs.txt文件,该文件保存着测试使用的图片,其中有同一个人,以及不同人的图片对;
  • 创建一个对象,使用TF的队列机制加载数据;
  • 加载facenet模型;
  • 启动QueueRunner,计算测试图片对的距离,根据距离(距离小于1为同一个人,否则相反)和实际标签来进行评估准确率;

5、在LFW数据集上使用已有模型

在实际应用过程中,我们有时候还会关心如何在自己的图像上应用已有模型。下面我们以计算人脸之间的距离为例,演示如何将模型应用到自己的数据上。

假设我们现在有三张图片,我们把他们存放在facenet/src目录下,文件分别叫做img1.jpg,img2.jpg,img3.jpg。这三张图像中各包含有一个人的人脸,我们希望计算它们两两之间的距离。使用facenet/src/compare.py文件来实现。

我们打开Anaconda Prompt,来到facenet路径下(注意这里是facenet路径下),运行如下命令:

python src/compare.py models/20180408-102900 src/img1.jpg  src/img2.jpg src/img3.jpg  

运行结果如下:

 

 我们尝试使用不同的三个人的图片进行测试:

python src/compare.py models/20180408-102900 src/img3.jpg  src/img4.jpg src/img5.jpg  

 我们会发现同一个人的图片,测试得到的距离值偏小,而不同的人测试得到的距离偏大。正常情况下同一个人测得距离应该小于1,不同人测得距离应该大于1。然而上面的结果却不是这样,我认为这多半与我们选取的照片有关。在选取测试照片时,我们尽量要选取脸部较为清晰并且端正的图片,并且要与训练数据具有相同分布的图片,即尽量选取一些外国人的图片进行测试。

python src/compare.py models/20180408-102900 src/img6.jpg  src/img7.jpg src/img8.jpg  

我们可以看到这个效果还是不错的,因此如果我们想在我们华人图片上也取得不错的效果,我们需要用华人的数据集进行训练模型。

compare.py源码如下:

 View Code

  • 首先使用MTCNN网络对原始测试图片进行检测和对齐,即得到[n,160,160,3]的输出;
  • 从模型文件(meta,ckpt文件)中加载facenet网络;
  • 把处理后的测试图片输入网络,得到每个图像的特征,对特征计算两两之间的距离以得到人脸之间的相似度;

6、重新训练新模型

从头训练一个新模型需要非常多的数据集,这里我们以CASIA-WebFace为例,这个 dataset 在原始地址已经下载不到了,而且这个 dataset 据说有很多无效的图片,所以这里我们使用的是清理过的数据库。该数据库可以在百度网盘有下载:下载地址,提取密码为 3zbb;或者在如下网址下载https://download.csdn.net/download/dsbhgkrgherk/10228992。

这个数据库有 10575 个类别494414张图像,每个类别都有各自的文件夹,里面有同一个人的几张或者几十张不等的脸部图片。我们先利用MTCNN 从这些照片中把人物的脸框出来,然后交给下面的 Facenet 去训练。

下载好之后,解压到datasets/casia/raw目录下,如图:

其中每个文件夹代表一个人,文件夹保存这个人的所有人脸图片。与LFW数据集类似,我们先利用MTCNN对原始图像进行人脸检测和对齐,我们打开Anaconda Prompt,来到facenet路径下,运行如下命令:

python  src/align/align_dataset_mtcnn.py   ../datasets/casia/raw  ../datasets/casia/casia_maxpy_mtcnnpy_182 --image_size 182 --margin 44 --random_order

对齐后的图像保存在路径datasets/casia/casia_maxpy_mtcnnpy_182下,每张图像的大小都是182×182182×182。而最终网络的输入是160×160160×160,之所以先生成182×182182×182的图像,是为了留出一定的空间给数据增强的裁切环节。我们会在182×182182×182的图像上随机裁切出160×160160×160的区域,再送入神经网络进行训练。

使用如下命令开始训练:

python src/train_softmax.py --logs_base_dir ./logs  --models_base_dir  ./models  --data_dir ../datasets/casia/casia_maxpy_mtcnnpy_182  --image_size 160 --model_def models.inception_resnet_v1 --lfw_dir ../datasets/lfw/lfw_mtcnnpy_160 --optimizer RMSPROP --learning_rate -1 --max_nrof_epochs 80 --keep_probability 0.8  --random_crop --random_flip --learning_rate_schedule_file data/learning_rate_schedule_classifier_casia.txt  --weight_decay 5e-5 --center_loss_factor 1e-2 --center_loss_alfa 0.9

上面命令中有很多参数,我们来一一介绍。首先是文件src/train_softmax.py文件,它采用中心损失和softmax损失结合来训练模型,其中参数如下:

  • --logs_base_dir./logs:将会把训练日志保存到./logs中,在运行时,会在./logs文件夹下新建一个以当前时间命名的文讲夹。最终的日志会保存在这个文件夹中,所谓的日志文件,实际上指的是tf中的events文件,它主要包含当前损失、当前训练步数、当前学习率等信息。后面我们会使用TensorBoard查看这些信息;
  • --models_base_dir ./models:最终训练好的模型保存在./models文件夹下,在运行时,会在./models文件夹下新建一个以当前时间命名的文讲夹,并用来保存训练好的模型;
  • --data_dir ../datasets/casis/casia_maxpy_mtcnnpy_182:指定训练所使用的数据集的路径,这里使用的就是刚才对齐好的CASIA-WebFace人脸数据;
  • --image_size 160:输入网络的图片尺寸是160×160160×160大小;
  • --mode_def models.inception_resnet_v1:指定了训练所使用的卷积网络是inception_resnet_v1网络。项目所支持的网络在src/models目录下,包含inception_resnet_v1,inception_resnet_v2和squeezenet三个模型,前两个模型较大,最后一个模型较小。如果在训练时出现内存或者显存不足的情况可以尝试使用sequeezenet网络,也可以修改batch_size 大小为32或者64(默认是90);
  • --lfw_dir  ../datasets/lfw/lfw_mtcnnpy_160:指定了LFW数据集的路径。如果指定了这个参数,那么每训练完一个epoch,就会在LFW数据集上执行一次测试,并将测试的准确率写入到日志文件中;
  • --optimizer RMSPROP :指定训练使用的优化方法;
  • --learning_rate  -1:指定学习率,指定了负数表示忽略这个参数,而使用后面的--learning_rate_schedule_file参数规划学习率;
  • --max_nrof_epochs 80:指定训练轮数epoch;
  • --keep_probability 0.8:指定弃权的神经元保留率;
  • --random_crop:表明在数据增强时使用随机裁切;
  • --random_flip :表明在数据增强时使用随机随机翻转;
  • --learning_rate_schedule_file data/learning_rate_schedule_classifier_casia.txt:在之前指定了--learning_rate  -1,因此最终的学习率将由参数--learning_rate_schedule_file决定。这个参数指定一个文件data/learning_rate_schedule_classifier_casia.txt,该文件内容如下:
    # Learning rate schedule
    # Maps an epoch number to a learning rate
    0:  0.05
    60: 0.005
    80: 0.0005
    91: -1
  • --weight_decay 5e-5:正则化系数;
  • --center_loss_factor 1e-2 :中心损失和Softmax损失的平衡系数;
  • --center_loss_alfa 0.9:中心损失的内部参数;

除了上面我们使用到的参数,还有许多参数,下面介绍一些比较重要的:

  • pretrained_model :models/20180408-102900  预训练模型,使用预训练模型可以加快训练速度(微调时经常使用到);
  • batch_size:batch大小,越大,需要的内存也会越大;
  • random_rotate:表明在数据增强时使用随机旋转;

由于CASIA-WebFace数据集比较大、训练起来周期较长,下面我们使用CASIA-WebFace一部分数据进行训练,运行结果如下:

其中Epoch:[32][683/1000]表示当前为第32个epoch以及当前epoch内的第683个训练batch,程序中默认参数epoch_size为1000,表示一个epoch有1000个batch。Time表示这一步的消耗的时间,Lr是学习率,Loss为当前batch的损失,Xent是softmax损失,RegLoss是正则化损失和中心损失之和,Cl是中心损失(注意这里的损失都是平均损失,即当前batch损失和/batch_size);

生成日志文件和模型文件:

我们启动Anaconda Prompt,首先来到日志文件的上级路径下,这一步是必须的,然后输入如下命令:

tensorboard --logdir E:\program\facenet\logs\20181007-12244

 

接着打开浏览器,输入http://127.0.0.1:6006,这里127.0.0.1是本机地址,6006是端口号。打开后,单击SCALARS,我们会看到我们在程序中创建的变量total_loss_1,点击它,会显示如下内容:

上图为训练过程中损失函数的变化过程,横坐标为迭代步数,这里为33k左右,主要是因为我迭代了33个epoch后终止了程序,每个epoch又迭代1000个batch。

与之对应的,每个epoch结束还会在LFW数据集上做一次验证,对应的准确率变化曲线如下:

在左侧有个smoothing滚动条,可以用来改变右侧标量的曲线,我们还可以勾选上show data download links,然后下载数据。

train_softmax.py源码如下:

 View Code

  • 创建CNN网络,用于提取人脸特征;
  • 训练数据准备阶段,采用TF队列机制加载数据集;
  • 定义CNN网络损失函数,L2L2正则化、中心损失函数、交叉熵代价函数(严格来说是softmax损失函数);
  • 开始在数据集上训练、并在LFW上测试;

7、三元组损失和中心损失的定义

最后我们来分析一下三元损失和中心损失的定义。

三元组损失的定义在文件src/facenet.py中,对应的函数为triplet_loss():

复制代码

def triplet_loss(anchor, positive, negative, alpha):
    """Calculate the triplet loss according to the FaceNet paper
    
    Args:
      anchor: the embeddings for the anchor images.
      positive: the embeddings for the positive images.
      negative: the embeddings for the negative images.
  
    Returns:
      the triplet loss according to the FaceNet paper as a float tensor.
    """
    with tf.variable_scope('triplet_loss'):
        pos_dist = tf.reduce_sum(tf.square(tf.subtract(anchor, positive)), 1)
        neg_dist = tf.reduce_sum(tf.square(tf.subtract(anchor, negative)), 1)
        
        basic_loss = tf.add(tf.subtract(pos_dist,neg_dist), alpha)
        loss = tf.reduce_mean(tf.maximum(basic_loss, 0.0), 0)
      
    return loss

复制代码

输入的anchor、positive、negative分别为随机选取的人脸样本的特征、anchor的正样本特征、anchor的负样本特征,它们的形状都是[batch_size,feature_size]。batch_size很好理解,feature_size是网络学习的人脸特征的维数。对应到三元组损失的公式Li=[∥f(xai)−f(xpi)∥22+α−∥f(xai)−f(xni)∥22]+Li=[‖f(xia)−f(xip)‖22+α−‖f(xia)−f(xin)‖22]+中的话,anchor每一行就是一个xaixia,positive的每行就是相应正样本的xpixip,negative每行就是负样本xnixin。先来分别计算正样本和负样本到anchor的L2L2距离。变量pos_dist就是anchor到各自正样本之间的距离∥f(xai)−f(xpi)∥22‖f(xia)−f(xip)‖22,变量neg_dist是anchor到负样本的距离∥f(xai)−f(xni)∥22‖f(xia)−f(xin)‖22。接下来,用pos_dist减去neg_dist再加上一个αα,最终损失只计算大于0的这部分,这和公式LiLi是完全一致的。

再来看一下中心损失的定义,同样是位于src/facenet.py中,对应的函数是center_loss():

复制代码

def center_loss(features, label, alfa, nrof_classes):
    """Center loss based on the paper "A Discriminative Feature Learning Approach for Deep Face Recognition"
       (http://ydwen.github.io/papers/WenECCV16.pdf)
    """
    #nrof_features就是feature_size,即CNN计算得到的人脸的维数
    nrof_features = features.get_shape()[1]
    
    #centers为变量,它是各个类别对应的类别中心
    centers = tf.get_variable('centers', [nrof_classes, nrof_features], dtype=tf.float32,
        initializer=tf.constant_initializer(0), trainable=False)
    
    label = tf.reshape(label, [-1])
    
    #根据label,取出features中每一个样本对应的类别中心
    #centers_batch形状和features一致,[batch_size,feature_size]
    centers_batch = tf.gather(centers, label)
    
    #计算类别中心和各个样本特征的差距diff
    #diff用来更新各个类别中心的位置
    #计算diff时用到的alfa是一个超参数,它可以控制中心位置的更新幅度
    diff = (1 - alfa) * (centers_batch - features)
    
    #使用diff来更新中心
    centers = tf.scatter_sub(centers, label, diff)
    
    #计算loss
    with tf.control_dependencies([centers]):
        loss = tf.reduce_mean(tf.square(features - centers_batch))
        
    #返回loss和更新后的中心
    return loss, centers

复制代码

输入参数features是样本的特征,它的形状为[batch_size,feature_size]。label为这些样本各自的类别标签号(即属于哪个人,这里使用0,1,2,3...表示),它的形状是[batch_size,]。alfa是一个超参数,它是0~1之间的一个浮点数。nrof_classes是一个整数,它表示全部训练集中样本的类别总数。

定义中心损失时,首先会根据各个样本的标签取出相应的类别中心center_batch、center_batch形状和feature完全一致,中心损失就是它们之间的L2L2距离。这与之前我们介绍的中心损失函数公式Li=12∥f(xi)−cyi∥22Li=12‖f(xi)−cyi‖22只差一个比例系数。此外程序还会计算center_batch和feature的差值diff,根据diff来更新类别中心,超参数alfa可以控制更新时的幅值。

参考文章:

[1]Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks

[2]官方代码

[3]其他代码实现(MXNet)

[4]21个项目玩转深度学习 何之源(部分内容来自于该书,第六章,GitHub网址)

[5]如何通过OpenFace实现人脸识别框架

[6]如何应用MTCNN和FaceNet模型实现人脸检测及识别(原理讲解还是比较细的)

[7]MTCNN 的 TensorFlow 实现

[8]人脸识别(Facenet)

[9]【数据库】FaceDataset常用的人脸数据库

你可能感兴趣的:(深度)