MTCNN算法簡介

 

 

主頁https://kpzhang93.github.io/MTCNN_face_detection_alignment/index.html
論文https://arxiv.org/abs/1604.02878
代碼官方matlab版C++ caffe版
第三方訓練代碼tensorflowmxnet

MTCNN,恰如論文標題《Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks》所言,採用級聯CNN結構,通過多任務學習,同時完成了兩個任務——人臉檢測和人臉對齊,輸出人臉的Bounding Box以及人臉的關鍵點(眼睛、鼻子、嘴)位置

MTCNN 又好又快,提出時在FDDBWIDER FACEAFLW數據集上取得了當時(2016年4月)最好的結果,速度又快,現在仍被廣泛使用作爲人臉識別的前端,如InsightFacefacenet

MTCNN效果爲什麼好,文中提了3個主要的原因:

  1. 精心設計的級聯CNN架構(carefully designed cascaded CNNs architecture)
  2. 在線困難樣本挖掘(online hard sample mining strategy)
  3. 人臉對齊聯合學習(joint face alignment learning)

下面詳細介紹。

 

模型概述

多任務卷積神經網絡(MTCNN)實現人臉檢測與對齊是在一個網絡裏實現了人臉檢測與五點標定的模型,主要是通過CNN模型級聯實現了多任務學習網絡。

整個模型分爲三個階段,第一階段通過一個淺層的CNN網絡快速產生一系列的候選窗口;第二階段通過一個能力更強的CNN網絡過濾掉絕大部分非人臉候選窗口;第三階段通過一個能力更加強的網絡找到人臉上面的五個標記點

總體而言,MTCNN方法可以概括爲:圖像金字塔+3階段級聯CNN,如下圖所示

 

對輸入圖像建立金字塔是爲了檢測不同尺度的人臉,通過級聯CNN完成對人臉 由粗到細(coarse-to-fine) 的檢測,所謂級聯指的是 前者的輸出是後者的輸入,前者往往先使用少量信息做個大致的判斷,快速將不是人臉的區域剔除,剩下可能包含人臉的區域交給後面更復雜的網絡,利用更多信息進一步篩選,這種由粗到細的方式在保證召回率的情況下可以大大提高篩選效率。下面爲MTCNN中級聯的3個網絡(P-Net、R-Net、O-Net),可以看到它們的網絡層數逐漸加深輸入圖像的尺寸(感受野)在逐漸變大12→24→48最終輸出的特徵維數也在增加32→128→256,意味着利用的信息越來越多。

 

工作流程

首先,對原圖通過雙線性插值構建圖像金字塔,可以參看前面的博文《人臉檢測中,如何構建輸入圖像金字塔》。構建好金字塔後,將金字塔中的圖像逐個輸入給P-Net。

  • P-Net:其實是個全卷積神經網絡(FCN),前向傳播得到的特徵圖在每個位置是個32維的特徵向量,用於判斷每個位置處約12×1212×12大小的區域內是否包含人臉,如果包含人臉,則迴歸出人臉的Bounding Box,進一步獲得Bounding Box對應到原圖中的區域,通過NMS保留分數最高的Bounding box以及移除重疊區域過大的Bounding Box。(
  • 網絡是全卷積神經網絡是一個推薦網絡簡稱 P-Net, 主要功能是獲得臉部區域的窗口與邊界Box迴歸,獲得的臉部區域窗口會通過BB迴歸的結果進行校正,然後使用非最大壓制(NMS)合併重疊窗口。)
  •  
  • R-Net:是單純的卷積神經網絡(CNN),先將P-Net認爲可能包含人臉的Bounding Box 雙線性插值到24×2424×24,輸入給R-Net,判斷是否包含人臉,如果包含人臉,也迴歸出Bounding Box,同樣經過NMS過濾。(
  • 網絡模型稱爲優化網絡R-Net,大量過濾非人臉區域候選窗口,然後繼續校正BB迴歸的結果,使用NMS進行合併。)
  •  
  • O-Net:也是純粹的卷積神經網絡(CNN),將R-Net認爲可能包含人臉的Bounding Box 雙線性插值到48×4848×48,輸入給O-Net,進行人臉檢測和關鍵點提取。(

    網絡模型稱爲O-Net,輸入第二階段數據進行更進一步的提取,最終輸出人臉標定的5個點位置。)

需要注意的是:

  1. face classification判斷是不是人臉使用的是softmax,因此輸出是2維的,一個代表是人臉,一個代表不是人臉
  2. bounding box regression迴歸出的是bounding box左上角和右下角的偏移𝑑𝑥1,𝑑𝑦1,𝑑𝑥2,𝑑𝑦2dx1,dy1,dx2,dy2,因此是4維的
  3. facial landmark localization迴歸出的是左眼、右眼、鼻子、左嘴角、右嘴角共5個點的位置,因此是10維的
  4. 訓練階段,3個網絡都會將關鍵點位置作爲監督信號來引導網絡的學習, 但在預測階段,P-Net和R-Net僅做人臉檢測,不輸出關鍵點位置(因爲這時人臉檢測都是不準的),關鍵點位置僅在O-Net中輸出。
  5. Bounding box關鍵點輸出均爲歸一化後的相對座標,Bounding Box是相對待檢測區域(R-Net和O-Net是相對輸入圖像),歸一化是相對座標除以檢測區域的寬高,關鍵點座標是相對Bounding box的座標,歸一化是相對座標除以Bounding box的寬高,這裏先建立起初步的印象,具體可以參看後面準備訓練數據部分和預測部分的代碼細節。

MTCNN效果好的第1個原因是精心設計的級聯CNN架構,其實,級聯的思想早已有之,而使用級聯CNN進行人臉檢測的方法是在2015 CVPR《A convolutional neural network cascade for face detection》中被率先提出,MTCNN與之的差異在於:

  • 減少卷積核數量(層內)
  • 將5×55×5的卷積核替換爲3×33×3
  • 增加網絡深度

這樣使網絡的表達能力更強,同時運行時間更少。

 

 

多任務學習與在線困難樣本挖掘

4種訓練數據參與的訓練任務如下:

  • Negatives和Positives用於訓練face classification
  • Positives和Part faces用於訓練bounding box regression
  • landmark faces用於訓練facial landmark localization

據此來設置𝛽𝑗𝑖βij,對每一個樣本看其屬於那種訓練數據,對其能參與的任務將𝛽β置爲1,不參與的置爲0。

至於在線困難樣本挖掘,僅在訓練face/non-face classification時使用,具體做法是:對每個mini-batch的數據先通過前向傳播,挑選損失最大的前70%作爲困難樣本,在反向傳播時僅使用這70%困難樣本產生的損失。文中的實驗表明,這樣做在FDDB數據級上可以帶來1.5個點的性能提升。

具體怎麼實現的?這裏以MTCNN-Tensorflow / train_models / mtcnn_model.py代碼爲例,用label來指示是哪種數據,下面爲代碼,重點關注valid_indslosssquare_error)的計算(對應𝛽𝑗𝑖βij),以及cls_ohem中的困難樣本挖掘

# in mtcnn_model.py]
# pos=1, neg=0, part=-1, landmark=-2
# 通過cls_ohem, bbox_ohem, landmark_ohem來計算損失
num_keep_radio = 0.7 # mini-batch前70%做爲困難樣本

# face/non-face 損失,注意在線困難樣本挖掘(前70%)
def cls_ohem(cls_prob, label):
    zeros = tf.zeros_like(label)
    #label=-1 --> label=0net_factory

    #pos -> 1, neg -> 0, others -> 0
    label_filter_invalid = tf.where(tf.less(label,0), zeros, label)
    num_cls_prob = tf.size(cls_prob)
    cls_prob_reshape = tf.reshape(cls_prob,[num_cls_prob,-1])
    label_int = tf.cast(label_filter_invalid,tf.int32)
    # get the number of rows of class_prob
    num_row = tf.to_int32(cls_prob.get_shape()[0])
    #row = [0,2,4.....]
    row = tf.range(num_row)*2
    indices_ = row + label_int
    label_prob = tf.squeeze(tf.gather(cls_prob_reshape, indices_))
    loss = -tf.log(label_prob+1e-10)
    zeros = tf.zeros_like(label_prob, dtype=tf.float32)
    ones = tf.ones_like(label_prob,dtype=tf.float32)
    # set pos and neg to be 1, rest to be 0
    valid_inds = tf.where(label < zeros,zeros,ones)
    # get the number of POS and NEG examples
    num_valid = tf.reduce_sum(valid_inds)

    ###### 困難樣本數量 #####
    keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    #FILTER OUT PART AND LANDMARK DATA
    loss = loss * valid_inds
    loss,_ = tf.nn.top_k(loss, k=keep_num) ##### 僅取困難樣本反向傳播 #####
    return tf.reduce_mean(loss)

# bounding box損失
#label=1 or label=-1 then do regression
def bbox_ohem(bbox_pred,bbox_target,label):
    '''

    :param bbox_pred:
    :param bbox_target:
    :param label: class label
    :return: mean euclidean loss for all the pos and part examples
    '''
    zeros_index = tf.zeros_like(label, dtype=tf.float32)
    ones_index = tf.ones_like(label,dtype=tf.float32)
    # keep pos and part examples
    valid_inds = tf.where(tf.equal(tf.abs(label), 1),ones_index,zeros_index)
    #(batch,)
    #calculate square sum
    square_error = tf.square(bbox_pred-bbox_target)
    square_error = tf.reduce_sum(square_error,axis=1)
    #keep_num scalar
    num_valid = tf.reduce_sum(valid_inds)
    #keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    # count the number of pos and part examples
    keep_num = tf.cast(num_valid, dtype=tf.int32)
    #keep valid index square_error
    square_error = square_error*valid_inds
    # keep top k examples, k equals to the number of positive examples
    _, k_index = tf.nn.top_k(square_error, k=keep_num)
    square_error = tf.gather(square_error, k_index)

    return tf.reduce_mean(square_error)

# 關鍵點損失
def landmark_ohem(landmark_pred,landmark_target,label):
    '''
    :param landmark_pred:
    :param landmark_target:
    :param label:
    :return: mean euclidean loss
    '''
    #keep label =-2  then do landmark detection
    ones = tf.ones_like(label,dtype=tf.float32)
    zeros = tf.zeros_like(label,dtype=tf.float32)
    valid_inds = tf.where(tf.equal(label,-2),ones,zeros) ##### 將label=-2的置爲1,其餘爲0 #####
    square_error = tf.square(landmark_pred-landmark_target)
    square_error = tf.reduce_sum(square_error,axis=1)
    num_valid = tf.reduce_sum(valid_inds)
    #keep_num = tf.cast(num_valid*num_keep_radio,dtype=tf.int32)
    keep_num = tf.cast(num_valid, dtype=tf.int32)
    square_error = square_error*valid_inds # 在計算landmark_ohem損失時只計算beta=1的 #####
    _, k_index = tf.nn.top_k(square_error, k=keep_num)
    square_error = tf.gather(square_error, k_index)
    return tf.reduce_mean(square_error)

 

多任務學習的代碼片段如下:

# in train.py
if net == 'PNet':
    image_size = 12
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 0.5;
elif net == 'RNet':
    image_size = 24
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 0.5;
else:
    radio_cls_loss = 1.0;radio_bbox_loss = 0.5;radio_landmark_loss = 1;
    image_size = 48

# ...
# 多任務聯合損失
total_loss_op  = radio_cls_loss*cls_loss_op + radio_bbox_loss*bbox_loss_op + radio_landmark_loss*landmark_loss_op + L2_loss_op
train_op, lr_op = train_model(base_lr, total_loss_op, num)

def train_model(base_lr, loss, data_num):
    """
    train model
    :param base_lr: base learning rate
    :param loss: loss
    :param data_num:
    :return:
    train_op, lr_op
    """
    lr_factor = 0.1
    global_step = tf.Variable(0, trainable=False)
    #LR_EPOCH [8,14]
    #boundaried [num_batch,num_batch]
    boundaries = [int(epoch * data_num / config.BATCH_SIZE) for epoch in config.LR_EPOCH]
    #lr_values[0.01,0.001,0.0001,0.00001]
    lr_values = [base_lr * (lr_factor ** x) for x in range(0, len(config.LR_EPOCH) + 1)]
    #control learning rate
    lr_op = tf.train.piecewise_constant(global_step, boundaries, lr_values)
    optimizer = tf.train.MomentumOptimizer(lr_op, 0.9)
    train_op = optimizer.minimize(loss, global_step)
    return train_op, lr_op

 

 

參考文獻:

1.https://www.cnblogs.com/shine-lee/p/10115582.html

2.如何應用MTCNN和FaceNet模型實現人臉檢測及識別

 

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