Tensorflow的图像操作(二)

Tensorflow的图像操作

度量学习

  • 什么是度量问题?

对于人脸匹配可以分为1:1和1:N。对于1:1的情况,我们可以采用分类模型,也可以采用度量模型。如果这两个1它们是同一个物体,在表示成特征向量的时候,这两个特征向量理论上是完全一样的两个特征向量,这两个特征向量的距离就是0。如果不同的两个向量,它们的距离可能就是∞。对于1:N的问题,主要就是采用度量的方法。比方说A和B同类,A和C不同类,则A和B的相似性大于A和C的相似性。我们在这里讨论的主要就是距离,如何去衡量两个向量之间的距离,这个距离我们将它定义为相似度。如果A和B的相似性达到了一定的程度,这时候我们就可以认为A和B是同类物体。基于这样一个前提,我们就可以去完成人脸度量以及去完成人脸识别。

距离的度量有非常多的方法,上图是几个比较具有代表性的方法。欧式距离的公式为,它考虑的是一种绝对距离,没有考虑到不同维度的值的变化的相关分布,具体可以参考机器学习算法整理 中的介绍。

马氏距离可以看作是欧氏距离的一种修正,公式为,其中Σ是多维随机变量的协方差矩阵,协方差就是计算两个不同的随机变量之间的相关程度的,具体可以参考概率论整理(二) 中的协方差及相关系数。如果协方差矩阵是单位向量,也就是各维度独立同分布,马氏距离就变成了欧式距离。但如果各维度不是独立同分布的,而是具有相关性,

上图的左下图中,样本的一个一个点就是一个一个的向量,我们将这些向量构成一个矩阵,将该矩阵进行SVD分解,即旋转、拉伸、旋转的过程就变成了右下图的样子。SVD分解可以参考线性代数整理(三) 矩阵的SVD分解。如果在左下图中,我们单纯的计算蓝点到中心绿点的欧式距离以及右上角的绿点到中心绿点的欧式距离,则蓝点的距离更近。但是经过了矩阵的SVD分解之后,我们可以看到情况就完全不一样了。蓝点明显是一个离散点,而非中心绿点距离中心绿点的距离更近。马氏距离除以了协方差矩阵,实际上就是把右上角的图变成了右下角,它是将概率论与线性代数做了一次完美的结合。

度量优化问题就是我们对空间去进行定义,这个定义类似于聚类 ,满足可分类/鉴别(同类之间距离最小,不同类之间距离最大)约束,稀疏性约束,噪声约束。这些条件都可以加在对空间的约束上。这个时候我们可以去求解计算距离的空间的时候,就变成了一个度量优化的问题。我们认为在加入了这些约束之后,对当前的这个空间去对特征向量进行相似性度量的时候,会计算出更加鲁棒的结果。这个时候就能够在一个很大的假设空间中找到一组合适的解,假设空间就是满足当前的条件的解。如果我们只是把样本拿来,有输入和输出,满足输入和输出的假设空间,就是满足输入和输出的参数模型是有非常多的,也就是说假设空间是非常多的。这个时候在进行度量优化的时候,就是对假设空间进行约束,在约束过之后就会对解的解空间会变小,变小之后会拿到更好的一组解。其实正则化的目的也是为了对解空间进行约束,进而防止产生过拟合的问题。正则化的概念可以参考机器学习算法整理(二) 模型泛化与岭回归。

除了上面的方法还可以采用深度学习去进行度量的优化。它可以通过网络来完成了对鉴别性信息的表示,通过网络来保证同类之间的距离尽可能的小,不同类之间的距离尽可能的大。此时就可以通过深度学习来对度量空间的约束。深度学习除了从度量的角度去考虑以外还可以去优化提取出来的特征向量去进行度量时所采用的方法,其中具有代表性的就是基于余弦距离设计出来的损失函数cos loss,这些loss会使得我们的向量在余弦距离这个距离下会满足相似性的约束。

我们在进行人脸特征匹配的时候,同样会采用欧式距离和余弦距离来测量提取出来的特征向量。特征向量会利用网络来进行学习。从深度学习来看人脸匹配的方法,此时就不再区分特征表示和度量这两个步骤,通常整个步骤都是通过一个网络来解决的。解决的方式,这里总结了三大类。一个是采用分类问题来进行人脸匹配的建模,将人脸匹配的问题定义为了人脸分类的问题,在分类的时候会采用softmax loss和一些余弦距离相关的loss。这些loss在进行特征学习和网络优化的时候会提取出相应的特征向量,而这些特征向量会针对不同的loss在进行度量的时候,会采用不同的策略。比如说采用softmax loss在进行学习的时候会采用欧式距离来对提取出来的特征进行相似性的度量。对于基于余弦loss提取出来的特征向量,会采用余弦距离来计算向量之间的相似度。

除了分类问题以外还有相似性网络,相似性网络和分类网络一个最大的区别就是相似性网络是多分支的,而分类问题是单支的。通过多支的网络结果对输入的样本进行改造,输入的样本有同类样本以及不同类样本,根据样本的id去设计不同的loss,如果是2支的会采用Contrastive loss,如果是3支的会采用Triplet loss。利用这两种loss同样能够提取出来特征,这个特征就具有鉴别信息,它能保证同类之间相似性很小,不同类相似性很大。在计算这些loss时通常会采用欧式距离来进行loss的计算。对于多分支的网络,在进行特征表示的时候,会进行不同分支之间参数共享,在网络收敛之后会取出其中的一支来提取特征,完成对图像特征向量的表示。然后再采用欧式距离来进行特征向量之间的相似性。

除了分类、相似,还有一类方式就是重排序,就是ReRank,就是在利用深度学习提取出来特征并且对特征进行了相似性度量,度量完进行了排序之后,根据排序的结果再重新进行排序。ReRank也是针对排序结果进行优化的很好的选择。这里以分类问题和相似性这两大类为主,并且会采用Trpilet Loss来完成实战。对于分类问题和相似性的选择,分类问题在样本量非常丰富,单类样本非常多,可以采用分类网络;如果样本数量比较少,单类样本量也比较少,这个时候可以采用相似性的多分支的网络结构。因为多分支的网络结构在输入的时候会构造样本对,这个样本对就会使原本很少的样本变得很丰富。当然在构造样本对也会带来正负样本比失衡的问题,很明显不同类的样本对比同类样本对数量是远远多的,这个时候会采用难例挖掘的方法来对模型在训练的时候进行优化。

这里列举出了在人脸匹配中的一些方法。分别是以欧式距离为主的方法以及余弦距离为主的方法。

FaceNet原理简介

  • learn to rank

FaceNet属于相似性多支的Triplet Loss的典型应用。Triplet Loss是一个3分支的网络结构,这3个分支分别对应到了三个输入样本,这三个样本有一个称之为Anchor,可以理解为一个参照。另外的两个样本以当前的Anchor来作为参照,如果认为跟它是同一类,那我们认为它是一个Positive;如果不同类,我们认为是一个Negative。对于Triplet Loss的三个分支,实际上就包含了一对相同类别的样本和一个不同类别的样本,这三个样本构成了一个三元组。而网络优化的方向就是对这个三元组来进行优化,使得同类样本的距离会远远小于不同类样本的距离。在上图中,我们的三元组在进行优化的过程称为Rank(排序),这个顺序其实就体现在了相似度上,经过对当前的三元组进行特征的优化,提取出来的特征能够使得同类样本之间的距离变小,不同类样本之间的距离变大。上图的左边的三元组我们会发现同类之间的距离大于不同类之间的距离,经过排序学习之后,右边的图中的三元组同类之间的距离小于不同类之间的距离,也就意味着同类样本之间的相似度会更高。如何完成这个特征提取和优化迭代的过程,取决于Triplet Loss的loss优化的方向。

这里需要注意的是上图的蓝点、红点、绿点是样本,但是它们也不是输入的图像,它实际上是对原始图像进行特征提取之后的特征。我们通过卷积神经网对三元组中的这三个样本进行特征提取,提取之后会得到不同的特征向量,每一个特征向量都表达了当前的一个样本。经过排序学习之后,能够保证在特征空间中,也就是特征向量它们之间的距离满足同类之间的距离是小于不同类之间的距离。换句话说就是同类样本之间的相似度会比不同类之间的相似度更高。

  • FaceNet网络结构

首先会有一个Batch size的数据,这个数据一定是多个三元组构成的。DEEP ARCHITECTURE就是主干网络,即为标准的卷积神经网(包含FC层),对输入图像进行特征提取的过程。通过卷积神经网对图片转化成向量,经过L2归一化,拿到Embedding(最终特征向量),作为Triplet Loss的输入来计算loss,这个输入同样也是三元组,包含了Anchor、Positive、Negative。拿到loss之后再对网络进行反向传播。最终特征学习和优化的方向取决于loss的设计

  • LOSS设计

这里有N个样本(三元组),表示Anchor样本的特征向量,表示Positive样本的特征向量,表示Negative样本的特征向量。那么这个损失函数就很清楚了,为Anchor的特征向量到Possitive的特征向量的欧式距离(同类样本之间的距离)减去Anchor的特征向量到Negative的特征向量的欧式距离(不同类样本之间的距离)。α是一个间隔,能够调节两种距离的差满足多大才能算作是一个正样本,也就是说这个距离差需要>-α。通过这个loss的设计学出来的特征就可以用来去度量样本的相似度Rank。

  • 难例挖掘

我们在构建样本对的时候,负样本对是远远大于正样本对的,这个时候就会带来另一种的正负样本分布不平衡。此时使用最多的就是OHEM。我们在进行loss计算的时候主要去考虑那些难例,对于那些容易分的样本就不计算loss。采用难例挖掘来尽可能的减少网络训练崩塌的问题,保证网络能够正常的收敛

  • 数据增强

为了提高网络的性能和鲁棒性,也会采用一些数据增强的策略。如颜色信息、几何变换、噪声、GAN、多样本合成类。

FaceNet环境搭建

框架下载地址:https://github.com/davidsandberg/facenet

人脸匹配数据准备

  1. LFW,常见人脸匹配数据集
  2. Celeba,不仅用于人脸匹配,在人脸属性方面也有非常多的应用
  3. VGGface2,一个非常大规模的数据集,数据量有40多G。
  4. CASIA-WebFace,亚洲人脸数据集
  5. CASIA-faceV5,亚洲人脸数据集,非常合适应用于当前任务。加入亚洲人脸会非常有利于模型性能的提升。LFW、Celeba、VGGface2以欧美人面孔为主,训练出来的模型用于当前任务会有一些精度上的损失。

基于这些公开数据集训练出来的模型同实际当前工业中用到的模型,差距主要是反映在数据上。实际上在解决人脸匹配任务在解决一个工业问题的时候,只有这些公开的数据集是远远不够的,需要去采集不同场景下的样本,比如说包括了晴天、雨天、阴天、白天、晚上、男人、女人、老人、小孩、中年人、有胡子、没胡子、戴眼镜、不戴眼镜、戴头纱、不戴头纱、戴帽子、不戴帽子等等这样一系列的不同场景,不同约束下的人脸,只有把这些人脸拿过来去进行训练的时候,我们才能拿到一个面向工业实际应用的模型。

那么当前基于这些公开数据集的训练意义是什么?通过公开的数据集,能够在不同的标准下衡量不同框架,可能是分类任务,可能是Triplet Loss多分支的网络结构,或者是一些其他的网络结构采用相同的数据集能够去判断当前的算法框架的瓶颈和优劣,以至于帮助我们来完成算法选型。在算法选型的时候,也就是常说的预研的环节,在进行了预研确定了技术方案之后再去补充数据,扩大数据规模在对模型进行进一步性能上的提升,进而满足工业场景高性能的要求。

  • 数据格式
  1. 文件夹名/文件夹名_文件名
  2. 同一个人的图片放在相同文件夹

LFW数据集下载地址链接: https://pan.baidu.com/s/1g0EpYe3d_vG5LnJhyB4Uig 提取码: p7ml

现在我们来看一下FaceNet源码算法框架,我们在facenet/src/align文件夹下可以看到一个align_dataset_mtcnn.py的脚本文件,这里是采用了mtcnn,通过mtcnn来检测人脸,以及给出人脸关键点来完成对人脸图像的裁减,根据检测出的人脸框和人脸位置来进行人脸抠图。然后将图片resize到160*160。

然后就是facenet/src目录下的train_tripletloss.py的脚本文件,为使用Triplet Loss人脸匹配训练脚本,在这个脚本中定义了我们需要的网络以及需要训练的参数。在facenet/src文件夹下还有一个train_softmax.py的脚本文件,为使用Softmax Loss人脸分类训练脚本。人脸匹配的重点在于如何去拿到一个具有可辨识性的特征向量。

freeze_graph.py为将训练好的模型转成pb文件的脚本。compare.py为比较两个图片的相似度的脚本。facenet.py包含了一些用到的网络结构以及一些主干网络结构。

在LFW数据集下,我们可以看到一些包含了人脸以及背景的图片,同一个文件夹下是同一个人的人脸。

 我们在进行人脸匹配的时候需要将这些人脸区域给抠出来,这时候就需要用到FaceNet提供的mtcnn的脚本,mtcnn是一个人脸检测关键点定位的多任务网络,它通过pnet、rnet和onet三个级联的网络来完成人脸检测和关键点定位,它们分别对应到了facenet/src/align目录下的det1.npy、det2.npy以及det3.npy三个文件。我们来看一下如何处理。

首先进入facenet的主目录/facenet下,执行命令

export PYTHONPATH=$(pwd)/src

将当前文件夹添加为环境变量。添加一个新的文件夹用来存放生成的文件

mkdir lfw_160

由于该框架代码是由tensorflow 1开发的,而一般我们安装的tensorflow环境为tensorflow 2的,所以在facenet/src/align目录下的align_dataset_mtcnn.py和detect_face.py的

import tensorflow as tf

修改成

import tensorflow.compat.v1 as tf

将detect_face.py的85行修改成

data_dict = np.load(data_path, encoding='latin1', allow_pickle=True).item() 

195行修改为

feed_in, dim = (inp, input_shape[-1])

在align_dataset_mtcnn.py中修改

import imageio
from PIL import Image

85行修改

img = imageio.imread(image_path)

 126行修改,这里需要注释掉scaled = misc.imresize(cropped, (args.image_size, args.image_size), interp='bilinear'),替换成上面三行代码

im = Image.fromarray(cropped)
size = (args.image_size, args.image_size)
scaled = np.array(im.resize(size, Image.BILINEAR))
# scaled = misc.imresize(cropped, (args.image_size, args.image_size), interp='bilinear')

136行修改为

imageio.imsave(output_filename_n, scaled)

然后在facenet目录下执行

python src/align/align_dataset_mtcnn.py /Users/admin/Downloads/lfw /Users/admin/Downloads/lfw_160 --image_size 160 --margin 32 --random_order

当然有GPU的可以执行

python src/align/align_dataset_mtcnn.py /Users/admin/Downloads/lfw /Users/admin/Downloads/lfw_160 --image_size 160 --margin 32 --random_order --gpu_memory_fraction 0.25

日志打印

Creating networks and loading parameters
2021-12-10 07:40:39.677279: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
WARNING:tensorflow:From /Users/admin/Downloads/facenet/facenet/src/align/detect_face.py:213: div (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Deprecated in favor of operator or tf.math.divide.
/Users/admin/Downloads/lfw/Lynne_Slepian/Lynne_Slepian_0001.jpg
/Users/admin/Downloads/lfw/Tom_Christerson/Tom_Christerson_0001.jpg
/Users/admin/Downloads/lfw/Robert_Downey_Jr/Robert_Downey_Jr_0001.jpg
/Users/admin/Downloads/lfw/Katie_Boone/Katie_Boone_0001.jpg
/Users/admin/Downloads/lfw/Nathan_Lane/Nathan_Lane_0001.jpg
/Users/admin/Downloads/lfw/Nathan_Lane/Nathan_Lane_0002.jpg
/Users/admin/Downloads/lfw/Michael_Winterbottom/Michael_Winterbottom_0001.jpg
/Users/admin/Downloads/lfw/Michael_Winterbottom/Michael_Winterbottom_0002.jpg

此时我们可以看到,它会将含有背景的图像提取成只有人脸的图像

不过这个过程是比较缓慢的。

Dlib处理CASIA-Face以及CELEBA

CASIA-faceV5数据集下载地址链接: https://pan.baidu.com/s/1WS4nooNQgmQHR6EpmrW6dw 密码: sc8b

在这里,我们同样可以看见不同的人的人脸图片在不同的文件夹下,每个文件夹下面有几个不同的人脸图片,它们属于同一个人,只不过都是亚洲人。

这里我们同样需要将这些人的人脸给抠取出来,采用之前的mtcnn是可以的,但是这里我们使用另外一种方法Dlib来进行抠取。首先安装所需要的库

pip install cmake
pip install dlib

新建一个文件夹,将几个分散文件夹中的图片全部放入该文件夹中,在64_CASIA-FaceV5目录下

mkdir image
cp -r CASIA-FaceV5\ \(000-099\)/* image/
cp -r CASIA-FaceV5\ \(100-199\)/* image/
cp -r CASIA-FaceV5\ \(200-299\)/* image/
cp -r CASIA-FaceV5\ \(300-399\)/* image/
cp -r CASIA-FaceV5\ \(400-499\)/* image/

新建一个图像抠取出来存放的文件夹

mkdir crop_image_160

在facenet/src/align目录下新建一个dlib_facedetect.py的脚本文件,内容如下

import cv2
import dlib
import glob
import os

if __name__ == "__main__":

    im_floder = "/Users/admin/Downloads/64_CASIA-FaceV5/image"
    crop_im_path = "/Users/admin/Downloads/64_CASIA-FaceV5/crop_image_160"
    im_folder_list = glob.glob(im_floder + "/*")
    # 定义人脸区域检测器
    detector = dlib.get_frontal_face_detector()
    idx = 0
    for idx_folder in im_folder_list:
        im_items_list = glob.glob(idx_folder + "/*")
        if not os.path.exists("{}/{}".format(crop_im_path, idx)):
            os.mkdir("{}/{}".format(crop_im_path, idx))
        idx_im = 0
        for im_path in im_items_list:
            im_data = cv2.imread(im_path)
            # 对人脸图像进行检测并获取检测结果
            dets = detector(im_data, 1)
            print(dets)
            if dets.__len__() == 0:
                continue
            # 获取第一个人脸框
            d = dets[0]
            # 获取人脸座标
            x1 = d.left()
            y1 = d.top()
            x2 = d.right()
            y2 = d.bottom()
            # 对人脸区域进行扩充
            y1 = int(y1 - (y2 - y1) * 0.3)
            x1 = int(x1 - (x2 - x1) * 0.05)
            x2 = int(x2 + (x2 - d.left()) * 0.05)
            y2 = y2
            # 进行裁剪
            im_crop_data = im_data[y1:y2, x1:x2]
            im_data = cv2.resize(im_crop_data, (160, 160))
            im_save_path = "{}/{}/{}_{}.jpg".format(crop_im_path, idx, idx, "%04d" % idx_im)
            cv2.imwrite(im_save_path, im_data)
            idx_im += 1
        idx += 1

运行后,我们可以在64_CASIA-FaceV5/crop_image_160文件夹下面看到提取出来的人脸图像

  • Celeba数据集

下载地址链接:  https://pan.baidu.com/s/1GBzQlRDyw3wxgxaDyMsNWA  提取码: wtna

facenet模型训练

由于该框架源码是由tensorflow 1开发的,而一般我们使用的是tensorflow 2的环境,所以框架有些地方需要修改

将facenet/src文件夹下的train_tripletloss.py、facenet.py以及facenet/src/models目录下的inception_resnet_v1.py的

import tensorflow as tf

替换成

import tensorflow.compat.v1 as tf

inception_resnet_v1.py中的

import tensorflow.contrib.slim as slim

替换成

import tf_slim as slim

train_tripletloss.py的429行,修改数据源路径,这里采用LFW数据源

parser.add_argument('--data_dir', type=str,
    help='Path to the data directory containing aligned face patches.',
    default='~/Downloads/lfw_160')

在facenet/src目录下执行命令

python train_tripletloss.py

训练日志

train_tripletloss.py:296: RuntimeWarning: invalid value encountered in less
  all_neg = np.where(neg_dists_sqr-pos_dist_sqr<alpha)[0] # VGG Face selecction
(nrof_random_negs, nrof_triplets) = (848, 848): time=32.640 seconds
Epoch: [0][1/1000]	Time 16.355	Loss 0.563
Epoch: [0][2/1000]	Time 6.026	Loss 0.576
Epoch: [0][3/1000]	Time 6.070	Loss 0.484
Epoch: [0][4/1000]	Time 6.064	Loss 0.653
Epoch: [0][5/1000]	Time 6.191	Loss 0.468
Epoch: [0][6/1000]	Time 6.098	Loss 0.590
Epoch: [0][7/1000]	Time 6.080	Loss 0.638
Epoch: [0][8/1000]	Time 6.083	Loss 0.453
Epoch: [0][9/1000]	Time 6.023	Loss 0.080
Epoch: [0][10/1000]	Time 6.118	Loss 0.287
Epoch: [0][11/1000]	Time 6.062	Loss 0.247
Epoch: [0][12/1000]	Time 6.061	Loss 0.183
Epoch: [0][13/1000]	Time 6.010	Loss 0.204
Epoch: [0][14/1000]	Time 6.089	Loss 0.095
Epoch: [0][15/1000]	Time 6.039	Loss 0.090
Epoch: [0][16/1000]	Time 6.048	Loss 0.058
Epoch: [0][17/1000]	Time 5.970	Loss 0.068
Epoch: [0][18/1000]	Time 5.971	Loss 0.031
Epoch: [0][19/1000]	Time 6.100	Loss 0.043

FaceNet源码解读与优化

我们来看一下train_tripletloss.py的源码,从main(args)方法开始

# 导入模型,主干网络,完成对图像的特征抽取
network = importlib.import_module(args.model_def)

在432行,我们可以看到这里的主干网络为inception_resnet_v1

parser.add_argument('--model_def', type=str,
    help='Model definition. Points to a module containing the definition of the inference graph.', default='models.inception_resnet_v1')

接下来是

# 创建日志和模型的存放文件夹
subdir = datetime.strftime(datetime.now(), '%Y%m%d-%H%M%S')
log_dir = os.path.join(os.path.expanduser(args.logs_base_dir), subdir)
if not os.path.isdir(log_dir):  # Create the log directory if it doesn't exist
    os.makedirs(log_dir)
model_dir = os.path.join(os.path.expanduser(args.models_base_dir), subdir)
if not os.path.isdir(model_dir):  # Create the model directory if it doesn't exist
    os.makedirs(model_dir)

这里的日志文件存放地点

parser.add_argument('--logs_base_dir', type=str, 
    help='Directory where to write event logs.', default='~/logs/facenet')

即我的电脑的/Users/admin/logs/facenet

cd /Users/admin/logs/facenet/
ls

我们可以看到日志文件存放的文件夹

20211211-064717	20211211-065643	20211211-075152	20211211-075229	20211211-082127

进入一个文件夹

cd 20211211-064717/
ls

可以看到里面的日志文件

arguments.txt		revision_info.txt

查看arguments.txt的内容为

logs_base_dir: ~/logs/facenet
models_base_dir: ~/models/facenet
gpu_memory_fraction: 1.0
pretrained_model: None
data_dir: ~/datasets/casia/casia_maxpy_mtcnnalign_182_160
model_def: models.inception_resnet_v1
max_nrof_epochs: 500
batch_size: 90
image_size: 160
people_per_batch: 45
images_per_person: 40
epoch_size: 1000
alpha: 0.2
embedding_size: 128
random_crop: False
random_flip: False
keep_probability: 1.0
weight_decay: 0.0
optimizer: ADAGRAD
learning_rate: 0.1
learning_rate_decay_epochs: 100
learning_rate_decay_factor: 1.0
moving_average_decay: 0.9999
seed: 666
learning_rate_schedule_file: data/learning_rate_schedule.txt
lfw_pairs: data/pairs.txt
lfw_dir: 
lfw_nrof_folds: 10

这里是训练的一些参数,模型文件的存放地点

parser.add_argument('--models_base_dir', type=str,
    help='Directory where to write trained models and checkpoints.', default='~/models/facenet')

即我的电脑的/Users/admin/models/facenet/,里面同样包含文件夹

20211211-064717	20211211-065643	20211211-075152	20211211-075229	20211211-082127

接下来是

# 将训练参数写入log文件中
facenet.write_arguments_to_file(args, os.path.join(log_dir, 'arguments.txt'))

这个我们在上面的日志文件中已经看到了,接下来是

# 存放git修订信息进log文件中
src_path, _ = os.path.split(os.path.realpath(__file__))
facenet.store_revision_info(src_path, log_dir, ' '.join(sys.argv))

这是另外一个log文件revision_info.txt的内容。接下来是

# 拿取训练集
np.random.seed(seed=args.seed)
train_set = facenet.get_dataset(args.data_dir)

这个训练集的地址就是我们之前修改过的

parser.add_argument('--data_dir', type=str,
    help='Path to the data directory containing aligned face patches.',
    default='~/Downloads/lfw_160')

facenet.get_dataset(args.data_dir)方法的实现在facenet.py中

def get_dataset(path, has_class_directories=True):
    dataset = []
    path_exp = os.path.expanduser(path)
    classes = [path for path in os.listdir(path_exp) \
                    if os.path.isdir(os.path.join(path_exp, path))]
    classes.sort()
    nrof_classes = len(classes)
    for i in range(nrof_classes):
        class_name = classes[i]
        facedir = os.path.join(path_exp, class_name)
        image_paths = get_image_paths(facedir)
        dataset.append(ImageClass(class_name, image_paths))
  
    return dataset

接下来

# 对参数的判断
print('Model directory: %s' % model_dir)
print('Log directory: %s' % log_dir)
if args.pretrained_model:
    print('Pre-trained model: %s' % os.path.expanduser(args.pretrained_model))

if args.lfw_dir:
    print('LFW directory: %s' % args.lfw_dir)
    # Read the file containing the pairs used for testing
    pairs = lfw.read_pairs(os.path.expanduser(args.lfw_pairs))
    # Get the paths for the corresponding images
    lfw_paths, actual_issame = lfw.get_paths(os.path.expanduser(args.lfw_dir), pairs)

接下来

# 构建图模型
with tf.Graph().as_default():
    tf.set_random_seed(args.seed)
    global_step = tf.Variable(0, trainable=False)

    # 梯度下降学习率,即步长
    learning_rate_placeholder = tf.placeholder(tf.float32, name='learning_rate')
    # 批数量
    batch_size_placeholder = tf.placeholder(tf.int32, name='batch_size')
    # 训练阶段
    phase_train_placeholder = tf.placeholder(tf.bool, name='phase_train')
    # 图片路径,shape=(None, 3)表示在tripletloss中每次训练的时候拿到的都是一个三元组
    image_paths_placeholder = tf.placeholder(tf.string, shape=(None, 3), name='image_paths')
    # 训练标签
    labels_placeholder = tf.placeholder(tf.int64, shape=(None, 3), name='labels')
    # 创建先进先出数据队列,通过数据队列完成对数据的读取
    input_queue = data_flow_ops.FIFOQueue(capacity=100000,
                                dtypes=[tf.string, tf.int64],
                                shapes=[(3,), (3,)],
                                shared_name=None, name=None)
    enqueue_op = input_queue.enqueue_many([image_paths_placeholder, labels_placeholder])
    
    nrof_preprocess_threads = 4
    images_and_labels = []
    for _ in range(nrof_preprocess_threads):
        # 对先进先出队列的读取,获取图片名称和标签
        filenames, label = input_queue.dequeue()
        images = []
        for filename in tf.unstack(filenames):
            # 读取图片
            file_contents = tf.read_file(filename)
            # 对图片进行编码
            image = tf.image.decode_image(file_contents, channels=3)
            # 数据增强
            if args.random_crop:
                # 随机裁剪
                image = tf.random_crop(image, [args.image_size, args.image_size, 3])
            else:
                # resize再裁剪和填充
                image = tf.image.resize_image_with_crop_or_pad(image, args.image_size, args.image_size)
            if args.random_flip:
                # 翻转
                image = tf.image.random_flip_left_right(image)

            # 设置图片大小
            image.set_shape((args.image_size, args.image_size, 3))
            # 图片数据标准化
            images.append(tf.image.per_image_standardization(image))
        images_and_labels.append([images, label])

这里只提供了两种数据增强的方法,我们可以自己去添加一些数据增强的方式,比如说增加对比度、亮度、噪声等方法来提高模型的鲁棒性,这是我们去进行优化的时候的一个地方。然后是

# 构造批处理(batch-size)数据
image_batch, labels_batch = tf.train.batch_join(
    images_and_labels, batch_size=batch_size_placeholder, 
    shapes=[(args.image_size, args.image_size, 3), ()], enqueue_many=True,
    capacity=4 * nrof_preprocess_threads * args.batch_size,
    allow_smaller_final_batch=True)
# 重新拷贝tensor,并进行重定义
image_batch = tf.identity(image_batch, 'image_batch')
image_batch = tf.identity(image_batch, 'input')
labels_batch = tf.identity(labels_batch, 'label_batch')

然后是

# 构建主干网络
prelogits, _ = network.inference(image_batch, args.keep_probability, 
    phase_train=phase_train_placeholder, bottleneck_layer_size=args.embedding_size,
    weight_decay=args.weight_decay)

我们来看一下inception_resnet_v1.py中的inference方法

def inference(images, keep_probability, phase_train=True, 
              bottleneck_layer_size=128, weight_decay=0.0, reuse=None):
    batch_norm_params = {
        # Decay for the moving averages.
        'decay': 0.995,
        # epsilon to prevent 0s in variance.
        'epsilon': 0.001,
        # force in-place updates of mean and variance estimates
        'updates_collections': None,
        # Moving averages ends up in the trainable variables collection
        'variables_collections': [ tf.GraphKeys.TRAINABLE_VARIABLES ],
    }
    
    with slim.arg_scope([slim.conv2d, slim.fully_connected],
                        weights_initializer=slim.initializers.xavier_initializer(), 
                        weights_regularizer=slim.l2_regularizer(weight_decay),
                        normalizer_fn=slim.batch_norm,
                        normalizer_params=batch_norm_params):
        return inception_resnet_v1(images, is_training=phase_train,
              dropout_keep_prob=keep_probability, bottleneck_layer_size=bottleneck_layer_size, reuse=reuse)

它返回了inception_resnet_v1网络结构,我们来看一下该网络结构的定义

def inception_resnet_v1(inputs, is_training=True,
                        dropout_keep_prob=0.8,
                        bottleneck_layer_size=128,
                        reuse=None, 
                        scope='InceptionResnetV1'):
    """Creates the Inception Resnet V1 model.
    Args:
      inputs: a 4-D tensor of size [batch_size, height, width, 3].
      num_classes: number of predicted classes.
      is_training: whether is training or not.
      dropout_keep_prob: float, the fraction to keep before final layer.
      reuse: whether or not the network and its variables should be reused. To be
        able to reuse 'scope' must be given.
      scope: Optional variable_scope.
    Returns:
      logits: the logits outputs of the model.
      end_points: the set of end_points from the inception model.
    """
    end_points = {}
  
    with tf.variable_scope(scope, 'InceptionResnetV1', [inputs], reuse=reuse):
        with slim.arg_scope([slim.batch_norm, slim.dropout],
                            is_training=is_training):
            with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
                                stride=1, padding='SAME'):
      
                # 149 x 149 x 32
                net = slim.conv2d(inputs, 32, 3, stride=2, padding='VALID',
                                  scope='Conv2d_1a_3x3')
                end_points['Conv2d_1a_3x3'] = net
                # 147 x 147 x 32
                net = slim.conv2d(net, 32, 3, padding='VALID',
                                  scope='Conv2d_2a_3x3')
                end_points['Conv2d_2a_3x3'] = net
                # 147 x 147 x 64
                net = slim.conv2d(net, 64, 3, scope='Conv2d_2b_3x3')
                end_points['Conv2d_2b_3x3'] = net
                # 73 x 73 x 64
                net = slim.max_pool2d(net, 3, stride=2, padding='VALID',
                                      scope='MaxPool_3a_3x3')
                end_points['MaxPool_3a_3x3'] = net
                # 73 x 73 x 80
                net = slim.conv2d(net, 80, 1, padding='VALID',
                                  scope='Conv2d_3b_1x1')
                end_points['Conv2d_3b_1x1'] = net
                # 71 x 71 x 192
                net = slim.conv2d(net, 192, 3, padding='VALID',
                                  scope='Conv2d_4a_3x3')
                end_points['Conv2d_4a_3x3'] = net
                # 35 x 35 x 256
                net = slim.conv2d(net, 256, 3, stride=2, padding='VALID',
                                  scope='Conv2d_4b_3x3')
                end_points['Conv2d_4b_3x3'] = net
                
                # 5 x Inception-resnet-A
                net = slim.repeat(net, 5, block35, scale=0.17)
                end_points['Mixed_5a'] = net
        
                # Reduction-A
                with tf.variable_scope('Mixed_6a'):
                    net = reduction_a(net, 192, 192, 256, 384)
                end_points['Mixed_6a'] = net
                
                # 10 x Inception-Resnet-B
                net = slim.repeat(net, 10, block17, scale=0.10)
                end_points['Mixed_6b'] = net
                
                # Reduction-B
                with tf.variable_scope('Mixed_7a'):
                    net = reduction_b(net)
                end_points['Mixed_7a'] = net
                
                # 5 x Inception-Resnet-C
                net = slim.repeat(net, 5, block8, scale=0.20)
                end_points['Mixed_8a'] = net
                
                net = block8(net, activation_fn=None)
                end_points['Mixed_8b'] = net
                
                with tf.variable_scope('Logits'):
                    end_points['PrePool'] = net
                    #pylint: disable=no-member
                    net = slim.avg_pool2d(net, net.get_shape()[1:3], padding='VALID',
                                          scope='AvgPool_1a_8x8')
                    net = slim.flatten(net)
          
                    net = slim.dropout(net, dropout_keep_prob, is_training=is_training,
                                       scope='Dropout')
          
                    end_points['PreLogitsFlatten'] = net
                
                net = slim.fully_connected(net, bottleneck_layer_size, activation_fn=None, 
                        scope='Bottleneck', reuse=False)
  
    return net, end_points

从上面的定义,我们可以看到它包含了各种卷积层、池化层以及inception的一些层,最终返回的是一个全连接层net = slim.fully_connected(net,bottleneck_layer_size, activation_fn=None, scope='Bottleneck', reuse=False),通过全连接层能够将输入的图片转化成一个向量,而end_points存放了整个网络的节点。我们继续来看train_tripletloss.py中的代码,接下来是

# 使用L2归一化得到最终embeddings特征向量
# 假设特征向量的维度是128,则这里是N * 128,N表示样本量
embeddings = tf.nn.l2_normalize(prelogits, 1, 1e-10, name='embeddings')
# 获取特征向量的三个分支
# 这里是将N * 128 reshape成了M * 3 * 128,这个M表示有多少个三元组
anchor, positive, negative = tf.unstack(tf.reshape(embeddings, [-1, 3, args.embedding_size]), 3, 1)
# 构建triplet loss损失函数
triplet_loss = facenet.triplet_loss(anchor, positive, negative, args.alpha)

这里我们来看一下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

这里我们可以看到它完全符合之前损失函数的定义

然后是

# 定义正则化的损失函数,主要用于批归一化
regularization_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
# 将triplet loss和正则化loss进行合并,拿到总的loss
total_loss = tf.add_n([triplet_loss] + regularization_losses, name='total_loss')

# 开始训练
train_op = facenet.train(total_loss, global_step, args.optimizer, 
    learning_rate, args.moving_average_decay, tf.global_variables())

这里我们来看一下facenet.py中的train方法

def train(total_loss, global_step, optimizer, learning_rate, moving_average_decay, update_gradient_vars, log_histograms=True):
    # Generate moving averages of all losses and associated summaries.
    loss_averages_op = _add_loss_summaries(total_loss)

    # 使用何种优化器
    with tf.control_dependencies([loss_averages_op]):
        if optimizer=='ADAGRAD':
            opt = tf.train.AdagradOptimizer(learning_rate)
        elif optimizer=='ADADELTA':
            opt = tf.train.AdadeltaOptimizer(learning_rate, rho=0.9, epsilon=1e-6)
        elif optimizer=='ADAM':
            opt = tf.train.AdamOptimizer(learning_rate, beta1=0.9, beta2=0.999, epsilon=0.1)
        elif optimizer=='RMSPROP':
            opt = tf.train.RMSPropOptimizer(learning_rate, decay=0.9, momentum=0.9, epsilon=1.0)
        elif optimizer=='MOM':
            opt = tf.train.MomentumOptimizer(learning_rate, 0.9, use_nesterov=True)
        else:
            raise ValueError('Invalid optimization algorithm')
        # 获取梯度
        grads = opt.compute_gradients(total_loss, update_gradient_vars)
        
    # 梯度下降
    apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
  
    # Add histograms for trainable variables.
    if log_histograms:
        for var in tf.trainable_variables():
            tf.summary.histogram(var.op.name, var)
   
    # Add histograms for gradients.
    if log_histograms:
        for grad, var in grads:
            if grad is not None:
                tf.summary.histogram(var.op.name + '/gradients', grad)
  
    # Track the moving averages of all trainable variables.
    variable_averages = tf.train.ExponentialMovingAverage(
        moving_average_decay, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
  
    with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
        train_op = tf.no_op(name='train')
  
    return train_op

这里基本上就是进行梯度下降的运算。然后是

    # 创建一个训练参数存储器
    saver = tf.train.Saver(tf.trainable_variables(), max_to_keep=3)

    # log日志信息
    summary_op = tf.summary.merge_all()

    # 定义GPU资源分配.
    gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=args.gpu_memory_fraction)
    # 按照GPU的分配来定义session
    sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))        

    # 对session的全局变量和局部变量进行初始化
    sess.run(tf.global_variables_initializer(), feed_dict={phase_train_placeholder: True})
    sess.run(tf.local_variables_initializer(), feed_dict={phase_train_placeholder: True})

    summary_writer = tf.summary.FileWriter(log_dir, sess.graph)
    # 开始数据读取的队列
    coord = tf.train.Coordinator()
    tf.train.start_queue_runners(coord=coord, sess=sess)

    with sess.as_default():
        # 是否使用预训练模型
        if args.pretrained_model:
            print('Restoring pretrained model: %s' % args.pretrained_model)
            # 对预训练模型的加载
            saver.restore(sess, os.path.expanduser(args.pretrained_model))

        # 对样本进行训练,这里会定义样本训练最大轮数args.max_nrof_epochs
        epoch = 0
        while epoch < args.max_nrof_epochs:
            step = sess.run(global_step, feed_dict=None)
            epoch = step // args.epoch_size
            # 每一轮训练一次
            train(args, sess, train_set, epoch, image_paths_placeholder, labels_placeholder, labels_batch,
                batch_size_placeholder, learning_rate_placeholder, phase_train_placeholder, enqueue_op, input_queue, global_step, 
                embeddings, total_loss, train_op, summary_op, summary_writer, args.learning_rate_schedule_file,
                args.embedding_size, anchor, positive, negative, triplet_loss)

            # 训练一轮后保存一次模型参数
            save_variables_and_metagraph(sess, saver, summary_writer, model_dir, subdir, step)

            # 训练一轮后会进行一次模型评测
            if args.lfw_dir:
                evaluate(sess, lfw_paths, embeddings, labels_batch, image_paths_placeholder, labels_placeholder, 
                        batch_size_placeholder, learning_rate_placeholder, phase_train_placeholder, enqueue_op, actual_issame, args.batch_size, 
                        args.lfw_nrof_folds, log_dir, step, summary_writer, args.embedding_size)

return model_dir
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章