1. 程式人生 > 其它 >[論文理解] 人臉識別論文總結(一)

[論文理解] 人臉識別論文總結(一)

Face Recognition Papers Review

Partial FC: Training 10 Million Identities on a Single Machine

arxiv: https://arxiv.org/pdf/2010.05222v2.pdf

主要兩個貢獻,一是把fc的權重存到不同卡上去,稱為model parallel, 二是隨機選擇negative pair來近似softmax的分母(很常規的做法)。

Model Parallel:

FC分類分配到n臺顯示卡上,每臺顯示卡分C/n 類,每張卡存權重的一部分,計算區域性每張卡上的exp和sumexp,然後互動計算softmax。

考慮梯度回傳問題,這樣做梯度也是parallel的,不同於資料parallel。資料parallel的話求梯度是需要用整個W才能求W的梯度的,而model paralle因為有了梯度公式,可知:

\[\nabla logits_i = prob_i - onehot_i \]

這一下明朗了,所以求權重\(W_i\)的梯度就有

\[\nabla W_i = X^T * \nabla logits_i \]

不需要整個W就可以求部分W的梯度啦。

作者覺得儘管model parallel了,但softmax的分母部分還是大啊,於是借鑑常用的無監督方法,隨機sample negative pairs,不需要全部的negative pair就可以估計出softmax的分母了。

An Efficient Training Approach for Very Large Scale Face Recognition

arxiv:https://arxiv.org/pdf/2105.10375v4.pdf

主要提出一種動態更新權重的池子方法,用單獨一個特徵網路來提取特徵作為權重,而非直接學全連線的權重,然後動態更新這個池子,就不需要儲存大量的權重了,加速了訓練、。

方法其實很樸素。

前面那個方法是把權重存到不同的GPU上去,因此,如果ID越來越多,我們加卡就可以了,但本文的方法不需要,也是節約成本的一個方法。

方法大致如下:

準備兩個網路,P網路用來訓練,G網路是P網路的moving avg,不訓練,最開始隨機初始化池子,記好當前batch的id,如果id在池子裡,訓練P網路用CE Loss,和cosine loss,如果不在用cosine loss,訓練一輪後更新G網路。G網路更新最老的池子,更新池子id。以此類推。

MagFace: A Universal Representation for Face Recognition and Quality Assessment

主要思想:

利用特徵的模長來表示樣本的質量,模長越小表明質量越低,並且設計損失函式希望無監督得達到這一目的。

文章用一個圖說明了傳統arcface存在的問題以及如何解決這一問題,對於第一個圖,作者認為傳統arcface理應對不同樣本設定不同的margin,對於質量高的樣本,他的margin應該大,而對於質量低的樣本,他的margin應該小,這是符合直覺的,高質量的樣本應該具有更好的區分度,而低質量的樣本由於其質量低可能區域性不確定,因此用小的margin更加合理;文章提出用向量模長來表示其質量,認為模長越大其質量越高。圖b則是根據不同模長動態設定margin,高質量樣本大margin,低質量樣本小margin,但這樣也存在一個問題,即質量低的樣本的可行域還是太大了,原文說是太free了,訓練是比較難收斂的;為了解決這一問題,文章提出對模長(質量)進行鼓勵,鼓勵高質量樣本的損失,即損失函式是模長的單調遞增的函式;再說一下圖c中m和g的影響,文章設計的m函式的作用如圖b其實是希望動態margin的同時固定住可行域,也就是圖b中的三角形的區域,對於圖c中的圖,低質量樣本2和3都超出了可行域,因此受m函式的影響會往可行域裡移動;g的設計是為了讓所有的樣本都儘可能貼近可行域的邊界,因此當兩個相反影響抵消時,其達到圖d的分佈。

m函式和g函式的設計:

\[\begin{aligned}&m\left(a_{i}\right)=\frac{u_{m}-l_{m}}{u_{a}-l_{a}}\left(a_{i}-l_{a}\right)+l_{m} \\&\lambda_{g} \geq \frac{s K}{-g^{\prime}\left(l_{a}\right)}=\frac{s u_{a}^{2} l_{a}^{2}}{\left(u_{a}^{2}-l_{a}^{2}\right)} \frac{u_{m}-l_{m}}{u_{a}-l_{a}}\end{aligned} \]

m函式通過模長限制了可行域為如圖所示的三角形區域,g函式是模長的雙曲線函式。

Adversarial Occlusion-aware Face Detection

這篇文章提出利用對抗訓練同時分割人臉遮擋區域和檢測人臉;

怎麼生成mask?

有三種方式:

  1. 根據關鍵點來在對應的feature上drop
  2. 隨機drop左右上下臉的feature
  3. 隨機drop一半的feature

何處對抗?

對於mask之後的feature,希望分類loss增大,沒有mask的loss減小。

CurricularFace: Adaptive Curriculum Learning Loss for Deep Face Recognition

文章主要三點貢獻

  1. 改進人臉識別的損失函式,利用課程學習幫助優化人臉識別
  2. 設計了一個指示函式來表明當前訓練的進度
  3. 大量實驗

主要目的是想要做到不同的訓練stage給easy sample和hard sample不同的權重,希望在訓練初期hard sample的權重要小一些,訓練後期hard sample的權重要大一些。

因此就涉及兩個問題,一個是訓練stage的劃分,如何指示訓練的stage,另一個是easy sample和hard sample的區分,如何區分兩者。

對於第一個問題,文章設計了一個指示函式:

由於發現了平均cos相似度可以一定程度反映訓練stage,早期顯然positive樣本由於訓練不充分所以大部分都是不相似的,訓練後期positive樣本訓練稍微充分,則相似性增大,因此可以用positive樣本的相似性近似表示訓練stage;此外,使用ema的方式防止stage的估計不穩定。

對於第二個問題,文章提出下面方式區分困難樣本和簡單樣本

對於(7)的第一行為簡單樣本,第二行為困難樣本,在訓練初期,t接近與0,N接近與cos的平方,比較小,訓練後期,t接近與1,顯然N會增大。

VarGNet: Variable Group Convolutional Neural Network for Efficient Embedded Computing

提出分組卷積的改進版,一般的分組卷積都是組數是超參,訓練時固定,根據輸入的channels的不同為每個組分配不同的channel,而VarGNet則是認為每個組應該處理的channel是超參,事先需要定下來,而在訓練中動態調整的則是組數,這樣導致的結果是,輸入的channel如果多,則組數多,輸入的channel少,則組數少。

我們知道,模型計算量和組數成反相關,

\[Cal = \frac{k^2 \times Height \times Width \times C_{in} \times C_{out}}{Groups} \]

所以在輸入通道過大時多用組數要計算更划算。

具體的網路結構上沒有指導,指導的是設計上的意義。

VarGFaceNet: An Efficient Variable Group Convolutional Neural Network for Lightweight Face Recognition

主要貢獻

  1. 提出用VarG卷積方式做backbone,並做了一些改進
  2. L2損失蒸餾
import torch
from torch import nn
from torchsummary import summary
from config import *
import math
import torch.nn.functional as F
from torch.nn import Parameter
'''
求Input的二範數,為其輸入除以其模長
角度蒸餾Loss需要用到
'''
def l2_norm(input, axis=1):
    norm  = torch.norm(input, axis, keepdim=True) # 預設p=2
    output = torch.div(input, norm)
    return output

'''
變組卷積,S表示每個通道的channel數量
'''
def VarGConv(in_channels, out_channels, kernel_size, stride, S):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding=kernel_size//2, groups=in_channels//S, bias=False),
        nn.BatchNorm2d(out_channels),
        nn.PReLU()
    )

'''
pointwise卷積,這裡的kernelsize都是1,不過這裡也要分組嗎??
'''
def PointConv(in_channels, out_channels, stride, S, isPReLU):
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, 1, stride, padding=0, groups=in_channels//S, bias=False),
        nn.BatchNorm2d(out_channels),
        nn.PReLU() if isPReLU else nn.Sequential()
    )

'''
SE block
'''
class SqueezeAndExcite(nn.Module):
    def __init__(self, in_channels, out_channels, divide=4):
        super(SqueezeAndExcite, self).__init__()
        mid_channels = in_channels // divide
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.SEblock = nn.Sequential(
            nn.Linear(in_features=in_channels, out_features=mid_channels),
            # nn.ReLU6(inplace=True),
            nn.ReLU6(inplace=False),
            nn.Linear(in_features=mid_channels, out_features=out_channels),
            # nn.ReLU6(inplace=True), # 其實這裡應該是sigmoid的
            nn.ReLU6(inplace=False)
        )

    def forward(self, x):
        b, c, h, w = x.size()
        out = self.pool(x)
        out = out.view(b, -1)
        out = self.SEblock(out)
        out = out.view(b, c, 1, 1)
        return out * x

'''
normal block
'''
class NormalBlock(nn.Module):
    def __init__(self, in_channels, kernel_size, stride=1, S=8):
        super(NormalBlock, self).__init__()
        out_channels = 2 * in_channels
        self.vargconv1 = VarGConv(in_channels, out_channels, kernel_size, stride, S)
        self.pointconv1 = PointConv(out_channels, in_channels, stride, S, isPReLU=True)

        self.vargconv2 = VarGConv(in_channels, out_channels, kernel_size, stride, S)
        self.pointconv2 = PointConv(out_channels, in_channels, stride, S, isPReLU=False)

        self.se = SqueezeAndExcite(in_channels, in_channels)
        self.prelu = nn.PReLU()

    def forward(self, x):
        out = x
        x = self.pointconv1(self.vargconv1(x))
        x = self.pointconv2(self.vargconv2(x))
        x = self.se(x)
        # out += x
        out = out + x
        return self.prelu(out)

'''
downsampling block
'''

class DownSampling(nn.Module):
    def __init__(self, in_channels, kernel_size, stride=2, S=8):
        super(DownSampling, self).__init__()
        out_channels = 2 * in_channels

        self.branch1 = nn.Sequential(
            VarGConv(in_channels, out_channels, kernel_size, stride, S),
            PointConv(out_channels, out_channels, 1, S, isPReLU=True)
        )

        self.branch2 = nn.Sequential(
            VarGConv(in_channels, out_channels, kernel_size, stride, S),
            PointConv(out_channels, out_channels, 1, S, isPReLU=True)
        )

        self.block3 = nn.Sequential(
            VarGConv(out_channels, 2*out_channels, kernel_size, 1, S), # stride =1
            PointConv(2*out_channels, out_channels, 1, S, isPReLU=False)
        ) # 上面那個分支

        self.shortcut = nn.Sequential(
            VarGConv(in_channels, out_channels, kernel_size, stride, S),
            PointConv(out_channels, out_channels, 1, S, isPReLU=False)
        )

        self.prelu = nn.PReLU()

    def forward(self, x):
        out = self.shortcut(x)

        x1 = x2 = x
        x1 = self.branch1(x1)
        x2 = self.branch2(x2)
        x3 = x1+x2
        x3 = self.block3(x3)

        # out += x3
        out = out + x3
        return self.prelu(out)

class HeadSetting(nn.Module):
    def __init__(self, in_channels, kernel_size, S=8):
        super(HeadSetting, self).__init__()
        self.block = nn.Sequential(
            VarGConv(in_channels, in_channels, kernel_size, 2, S),
            PointConv(in_channels, in_channels, 1, S, isPReLU=True),
            VarGConv(in_channels, in_channels, kernel_size, 1, S),
            PointConv(in_channels, in_channels, 1, S, isPReLU=False)
        )

        self.short = nn.Sequential(
            VarGConv(in_channels, in_channels, kernel_size, 2, S),
            PointConv(in_channels, in_channels, 1, S, isPReLU=False),
        )

    def forward(self, x):
        out = self.short(x)
        x = self.block(x)
        # out += x
        out = out + x
        return out

class Embedding(nn.Module):
    def __init__(self, in_channels, out_channels=512, S=8):
        super(Embedding, self).__init__()
        self.embedding = nn.Sequential(
            nn.Conv2d(in_channels, 1024, kernel_size=1, stride=1,padding=0, bias=False),
            nn.BatchNorm2d(1024),
            # nn.ReLU6(inplace=True),
            nn.ReLU6(inplace=False),
            nn.Conv2d(1024, 1024, (7, 6), 1, padding=0, groups=1024//8, bias=False),
            nn.Conv2d(1024, 512, 1, 1, padding=0, groups=512, bias=False)
        )

        self.fc = nn.Linear(in_features=512, out_features=out_channels)

    def forward(self, x):
        x = self.embedding(x)
        x = x.view(x.size(0), -1)
        out = self.fc(x)
        return out

class VarGFaceNet(nn.Module):
    def __init__(self, num_classes=512):
        super(VarGFaceNet, self).__init__()
        S=8
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=40, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(40),
            # nn.ReLU6(inplace=True)
            nn.ReLU6(inplace=False)
        )
        self.head = HeadSetting(40, 3)
        self.stage2 = nn.Sequential( # 1 normal 2 down
            DownSampling(40, 3, 2),
            NormalBlock(80, 3, 1),
            NormalBlock(80, 3, 1)
        )

        self.stage3 = nn.Sequential(
            DownSampling(80, 3, 2),
            NormalBlock(160, 3, 1),
            NormalBlock(160, 3, 1),
            NormalBlock(160, 3, 1),
            NormalBlock(160, 3, 1),
            NormalBlock(160, 3, 1),
            NormalBlock(160, 3, 1),
        )

        self.stage4 = nn.Sequential(
            DownSampling(160, 3, 2),
            NormalBlock(320, 3, 1),
            NormalBlock(320, 3, 1),
            NormalBlock(320, 3, 1),
        )

        self.embedding = Embedding(320, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    def forward(self, x):
        x = self.conv1(x)
        x = self.head(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)

        out = self.embedding(x)
        return out

Probabilistic Face Embeddings

文章提出對人臉進行概率建模,傳統方法只是針對某一輸入的人臉影象將其對映到隱空間的一個點,缺陷在於實際場景下可能出現低質量樣本,如果這些低質量樣本進入了人臉識別系統,人臉識別系統可能對將這些低質量樣本判錯,比較極端的是將所有低質量樣本分為同一個id。

為了解決這一問題,文章覺得應該有一個指標來告訴我們哪些樣本質量低,哪些樣本質量高,於是自然可以想到把一張圖片經過神經網路之後的輸出建模成一個概率分佈而不單單是一個點,這樣做的好處是可以利用其方差的交集大小來作為質量評估的標準,從直覺上我們可以認為方差比較大的樣本其質量可能不好,而方差比較小的樣本由於其確定性所以可認為其質量好。

此外,為了不破壞傳統模型的訓練方式引入新的訓練方法,本文直接在原有的訓練方式上做的改進,原來訓練網路到embedding層,然後是分類損失,當訓練完之後,認為該網路有了最為確定的影象中心(即網路的embedding輸出,這一輸出可認為是概率建模的均值),然後固定特徵提取網路,在後面加兩個全連線層去優化得到影象分佈方差。

優化方差的損失函式如下:

問題:為啥這個公式可以得到樣本方差?

其實很簡單,兩層的神經網路預測高斯分佈的方差,embedding是高斯分佈的均值,因此整個高斯分佈是可以確定的,所有需要最大化後驗概率,也就是兩個相同的樣本之間的後驗概率,第一項相當於對中心的距離加權,樣本距離越大會導致方差越大,後一項是對方差的懲罰項。

由於高質量樣本有懲罰低質量樣本沒有,所以會導致高質量樣本具有小方差。