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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章