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因為有了梯度公式,可知:
這一下明朗了,所以求權重\(W_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函數的設計:
m函數通過模長限制了可行域為如圖所示的三角形區域,g函數是模長的雙曲線函數。
Adversarial Occlusion-aware Face Detection
這篇文章提出利用對抗訓練同時分割人臉遮擋區域和檢測人臉;
怎么生成mask?
有三種方式:
- 根據關鍵點來在對應的feature上drop
- 隨機drop左右上下臉的feature
- 隨機drop一半的feature
何處對抗?
對於mask之后的feature,希望分類loss增大,沒有mask的loss減小。
CurricularFace: Adaptive Curriculum Learning Loss for Deep Face Recognition
文章主要三點貢獻
- 改進人臉識別的損失函數,利用課程學習幫助優化人臉識別
- 設計了一個指示函數來表明當前訓練的進度
- 大量實驗
主要目的是想要做到不同的訓練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少,則組數少。
我們知道,模型計算量和組數成反相關,
所以在輸入通道過大時多用組數要計算更划算。
具體的網絡結構上沒有指導,指導的是設計上的意義。
VarGFaceNet: An Efficient Variable Group Convolutional Neural Network for Lightweight Face Recognition
主要貢獻
- 提出用VarG卷積方式做backbone,並做了一些改進
- 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是高斯分布的均值,因此整個高斯分布是可以確定的,所以需要最大化后驗概率,也就是兩個相同的樣本之間的后驗概率,第一項相當於對中心的距離加權,樣本距離越大會導致方差越大,后一項是對方差的懲罰項。
由於高質量樣本有懲罰低質量樣本沒有,所以會導致高質量樣本具有小方差。