聰明的人臉識别1——Keras 搭建自己的Facenet人臉識别平台
- 學習前言
- 什麼是Facenet
- 源碼下載下傳
- Facenet的實作思路
-
- 一、預測部分
-
- 1、主幹網絡介紹
- 2、根據初步特征獲得長度為128的特征向量
- 3、l2标準化
- 4、建構分類器(用于輔助Triplet Loss的收斂)
- 二、訓練部分
-
- 1、資料集介紹
- 2、LOSS組成
- 訓練自己的Facenet人臉識别算法
學習前言
最近又學了我最喜歡的retinaface,以前也了解過facenet,但是沒有訓練過,可以去學習一下!
什麼是Facenet
谷歌人臉識别算法,發表于 CVPR 2015,利用相同人臉在不同角度等姿态的照片下有高内聚性,不同人臉有低耦合性,提出使用 cnn + triplet mining 方法,在 LFW 資料集上準确度達到 99.63%。
通過 CNN 将人臉映射到歐式空間的特征向量上,實質上:不同圖檔人臉特征的距離較大;通過相同個體的人臉的距離,總是小于不同個體的人臉這一先驗知識訓練網絡。
測試時隻需要計算人臉特征EMBEDDING,然後計算距離使用門檻值即可判定兩張人臉照片是否屬于相同的個體。
簡單來講,在使用階段,facenet即是:
1、輸入一張人臉圖檔
2、通過深度卷積網絡提取特征
3、L2标準化
4、得到一個長度為128特征向量。
源碼下載下傳
https://github.com/bubbliiiing/facenet-keras
Facenet的實作思路
一、預測部分
1、主幹網絡介紹
facenet的主幹網絡起到提取特征的作用,原版的facenet以Inception-ResNetV1為主幹特征提取網絡。
本文一共提供了兩個網絡作為主幹特征提取網絡,分别是mobilenetv1和Inception-ResNetV1,二者都起到特征提取的作用,為了友善了解,本博文中會使用mobilenetv1作為主幹特征提取網絡。
MobilenetV1模型是Google針對手機等嵌入式裝置提出的一種輕量級的深層神經網絡,其使用的核心思想便是depthwise separable convolution(深度可分離卷積塊)。
深度可分離卷積塊由兩個部分組成,分别是深度可分離卷積和1x1普通卷積,深度可分離卷積的卷積核大小一般是3x3的,便于了解的話我們可以把它當作是特征提取,1x1的普通卷積可以完成通道數的調整。
下圖為深度可分離卷積塊的結構示意圖:
深度可分離卷積塊的目的是使用更少的參數來代替普通的3x3卷積。
我們可以進行一下普通卷積和深度可分離卷積塊的對比:
對于普通卷積而言,假設有一個3×3大小的卷積層,其輸入通道為16、輸出通道為32。具體為,32個3×3大小的卷積核會周遊16個通道中的每個資料,最後可得到所需的32個輸出通道,所需參數為16×32×3×3=4608個。
對于深度可分離卷積結構塊而言,假設有一個深度可分離卷積結構塊,其輸入通道為16、輸出通道為32,其會用16個3×3大小的卷積核分别周遊16通道的資料,得到了16個特征圖譜。在融合操作之前,接着用32個1×1大小的卷積核周遊這16個特征圖譜,所需參數為16×3×3+16×32×1×1=656個。
可以看出來深度可分離卷積結構塊可以減少模型的參數。
如下就是MobileNet的結構,其中Conv dw就是分層卷積,在其之後都會接一個1x1的卷積進行通道處理,
import math
import numpy as np
import tensorflow as tf
from keras import backend
from keras import backend as K
from keras.preprocessing import image
from keras.models import Model
from keras.layers.normalization import BatchNormalization
from keras.layers import Conv2D, Add, ZeroPadding2D, GlobalAveragePooling2D, Dropout, Dense, Lambda
from keras.layers import MaxPooling2D,Activation,DepthwiseConv2D,Input,GlobalMaxPooling2D
from keras.applications import imagenet_utils
from keras.applications.imagenet_utils import decode_predictions
from keras.utils.data_utils import get_file
def _conv_block(inputs, filters, kernel=(3, 3), strides=(1, 1)):
x = Conv2D(filters, kernel,
padding='same',
use_bias=False,
strides=strides,
name='conv1')(inputs)
x = BatchNormalization(name='conv1_bn')(x)
return Activation(relu6, name='conv1_relu')(x)
def _depthwise_conv_block(inputs, pointwise_conv_filters,
depth_multiplier=1, strides=(1, 1), block_id=1):
x = DepthwiseConv2D((3, 3),
padding='same',
depth_multiplier=depth_multiplier,
strides=strides,
use_bias=False,
name='conv_dw_%d' % block_id)(inputs)
x = BatchNormalization(name='conv_dw_%d_bn' % block_id)(x)
x = Activation(relu6, name='conv_dw_%d_relu' % block_id)(x)
x = Conv2D(pointwise_conv_filters, (1, 1),
padding='same',
use_bias=False,
strides=(1, 1),
name='conv_pw_%d' % block_id)(x)
x = BatchNormalization(name='conv_pw_%d_bn' % block_id)(x)
return Activation(relu6, name='conv_pw_%d_relu' % block_id)(x)
def relu6(x):
return K.relu(x, max_value=6)
def MobileNet(inputs, embedding_size=128, dropout_keep_prob=0.8, alpha=1.0, depth_multiplier=1):
x = _conv_block(inputs, 32, strides=(2, 2))
x = _depthwise_conv_block(x, 64, depth_multiplier, block_id=1)
x = _depthwise_conv_block(x, 128, depth_multiplier, strides=(2, 2), block_id=2)
x = _depthwise_conv_block(x, 128, depth_multiplier, block_id=3)
x = _depthwise_conv_block(x, 256, depth_multiplier, strides=(2, 2), block_id=4)
x = _depthwise_conv_block(x, 256, depth_multiplier, block_id=5)
x = _depthwise_conv_block(x, 512, depth_multiplier, strides=(2, 2), block_id=6)
x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=7)
x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=8)
x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=9)
x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=10)
x = _depthwise_conv_block(x, 512, depth_multiplier, block_id=11)
x = _depthwise_conv_block(x, 1024, depth_multiplier, strides=(2, 2), block_id=12)
x = _depthwise_conv_block(x, 1024, depth_multiplier, block_id=13)
2、根據初步特征獲得長度為128的特征向量
利用主幹特征提取網絡我們可以獲得一個特征層,它的shape為(batch_size, h, w, channels),我們可以将其取全局平均池化,友善後續的處理(batch_size, channels)。
我們可以将平鋪後的特征層進行一個神經元個數為128的全連接配接。此時我們相當于利用了一個長度為128的特征向量代替輸入進來的圖檔。這個長度為128的特征向量就是輸入圖檔的特征濃縮。
x = GlobalAveragePooling2D()(x)
x = Dropout(1.0 - dropout_keep_prob, name='Dropout')(x)
x = Dense(classes, use_bias=False, name='Bottleneck')(x)
x = BatchNormalization(momentum=0.995, epsilon=0.001, scale=False,
name='BatchNorm_Bottleneck')(x)
3、l2标準化
在獲得一個長度為128的特征向量後,我們還需要進行l2标準化的處理。
這個L2标準化是為了使得不同人臉的特征向量可以屬于同一數量級,友善比較。
在進行l2标準化前需要首先計算2-範數:
∣ ∣ x ∣ ∣ 2 = ∑ i = 1 N x i 2 ||\textbf{x}||_2 =\sqrt{\sum_{i=1}^Nx_i^2} ∣∣x∣∣2=∑i=1Nxi2
,也就是歐幾裡得範數,即向量元素絕對值的平方和再開方。
L2标準化就是每個元素/L2範數;
在keras代碼中,隻需要一行就可以實作l2标準化的層。
x= Lambda(lambda x: K.l2_normalize(x, axis=-1))(x)
# 建立模型
model = Model(inputs, x, name='inception_resnet_v1')
到這裡,我們輸入進來的圖檔,已經變成了一個經過l2标準化的長度為128的特征向量了!
4、建構分類器(用于輔助Triplet Loss的收斂)
當我們完成第三步後,我們已經可以利用這個預測結果進行訓練和預測了。
但是由于僅僅隻是用Triplet Loss會使得整個網絡難以收斂,本文結合Cross-Entropy Loss和Triplet Loss作為總體loss。
Triplet Loss用于進行不同人的人臉特征向量歐幾裡得距離的擴張,同一個人的不同狀态的人臉特征向量歐幾裡得距離的縮小。
Cross-Entropy Loss用于人臉分類,具體作用是輔助Triplet Loss收斂。
想要利用Cross-Entropy Loss進行訓練需要建構分類器,是以對第三步獲得的結果再次進行一個全連接配接用于分類。
建構代碼如下,當我們在進行網絡的訓練的時候,可使用分類器輔助訓練,在預測的時候,分類器是不需要的:
def facenet(input_shape, num_classes=None, backbone="mobilenet", mode="train"):
inputs = Input(shape=input_shape)
if backbone=="mobilenet":
model = MobileNet(inputs)
elif backbone=="inception_resnetv1":
model = InceptionResNetV1(inputs)
else:
raise ValueError('Unsupported backbone - `{}`, Use mobilenet, inception_resnetv1.'.format(backbone))
if mode == "train":
x = Dense(num_classes)(model.output)
x = Activation("softmax", name = "Softmax")(x)
combine_model = Model(inputs,[x, model.output])
return combine_model
elif mode == "predict":
return model
else:
raise ValueError('Unsupported mode - `{}`, Use train, predict.'.format(mode))
二、訓練部分
1、資料集介紹
我們使用的資料集是CASIA-WebFace資料集,我已經對其進行了預處理,将其屬于同一個人的圖檔放到同一個檔案夾裡面,并且進行了人臉的提取和人臉的矯正。
資料集裡面有很多的檔案夾,每一個檔案夾裡面存放同一個人的不同情況下的人臉。不同檔案夾存放不同人的臉。
這是\0000045檔案夾裡面的人臉,屬于同一個人。
這是\0000099檔案夾裡面的人臉,屬于同一個人。
2、LOSS組成
facenet使用Triplet Loss作為loss。
Triplet Loss的輸入是一個三元組
- a:anchor,基準圖檔獲得的128維人臉特征向量
- p:positive,與基準圖檔屬于同一張人臉的圖檔獲得的128維人臉特征向量
- n:negative,與基準圖檔不屬于同一張人臉的圖檔獲得的128維人臉特征向量
我們可以将anchor和positive求歐幾裡得距離,并使其盡量小。
我們可以将negative和positive求歐幾裡得距離,并使其盡量大。
我們所使用的公式為。
L = m a x ( d ( a , p ) − d ( a , n ) + m a r g i n , 0 ) L=max(d(a,p)−d(a,n)+margin,0) L=max(d(a,p)−d(a,n)+margin,0)
d(a,p)就是anchor和positive的歐幾裡得距離。
d(a,n)就是negative和positive的歐幾裡得距離。
margin是一個常數。
d(a,p)前面為正符号,是以我們期望其越來越小。
d(a,n)前面為負符号,是以我們期望其越來越大。
即我們希望,同一個人的不同狀态的人臉特征向量歐幾裡得距離小。
不同人的人臉特征向量歐幾裡得距離大。
但是由于僅僅隻是用Triplet Loss會使得整個網絡難以收斂,本文結合Cross-Entropy Loss和Triplet Loss作為總體loss。
Triplet Loss用于進行不同人的人臉特征向量歐幾裡得距離的擴張,同一個人的不同狀态的人臉特征向量歐幾裡得距離的縮小。
Cross-Entropy Loss用于人臉分類,具體作用是輔助Triplet Loss收斂。
訓練自己的Facenet人臉識别算法
部落格中所使用的例子為CASIA-WebFace資料集。
下載下傳資料集,放在根目錄下的dataset檔案夾下。
運作根目錄下的txt_annotation.py,生成訓練所需的cls_train.txt。
cls_train.txt中每一行都存放了一張圖檔和它對應的類别(需要類别是因為訓練時會用交叉熵損失輔助收斂。)
下載下傳facenet_inception_resnetv1.h5或者facenet_mobilenet.h5放在model_data檔案夾内。
在train.py中指定合适的模型預訓練權重路徑。facenet_inception_resnetv1.h5是我已經訓練過的基于inception_resnetv1的facenet網絡;
facenet_mobilenet.h5是我已經訓練過的基于mobilenet的facenet網絡。
運作train.py開始訓練。