1.实验目的:
1.1 背景
人脸识别已经在生活中快速的普及开来, 但是人脸识别技术在实际应用中遇到的一个广为人知的问题是它在不同人种的性能有差异。 如何快速的提升人脸识别系统在不同人种的性能, 是一个实用的人脸识别算法应该考虑的问题。
1.2 实验目标
本次作业目标是提高人脸识别模型在不同人种上面的性能。以人脸1:1 比对为场景, 需要同时优化人脸识别模型在不同人种上的性能,提高在低误识率情况下不同人种的通过率。
总体目标:给两张图片,可以给出两张图片(这两张图片属于同一人种)上人脸的相似度,最终给出的相似度应在0~1
1.3 实验模型
使用的模型:ResNet ,MobileNet
ResNet的思想:神经网络固有的特性,并不是层数越多,效果越好,所以为了减少模型的复杂度,采取一种shortcut的思想
MobileNet的思想:是一种轻量级网络,可以在移动设备上运行
这里我尝试了ResNet18,ResNet24,ResNet50,ResNet101,ResNet152,MobileNet六种网络。
残差块有BasicBlock,Bootleneck,SEBlock,IRBlock四种。而在人脸识别领域,效果最好,采用最多的就是IRblock,所以这里都采用了这种残差块 ,具体的残差块实现在附录部分。
网络结构 | 残差块 |
---|---|
ResNet18 | IRblock |
ResNet24 | IRblock |
ResNet50 | IRblock |
ResNet101 | IRblock |
ResNet152 | IRblock |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRZ4Hr49-1571978683684)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
2.实验环境
硬件环境 :Tesla K80,
软件环境 :matplotlib,scipy,tqdm,opencv-python,pillow,torch,torchvision,numpy,scikit-image,torchsummary,tensorboardX,imgaug
3.实验过程
3.1 数据
- 训练集
原始数据集
人种 | 图片总数量 | 人数 |
---|---|---|
African | 3863 | 1543 |
Asian | 4947 | 1728 |
Caucasian | 271,354 | 11,326 |
Indian | 5182 | 1923 |
a.含有目标人种的数据,总共有四个目标人种。
b.每个目标人种含有若干文件夹, 每个文件夹是一个人。
c.每个文件夹内有若干对齐好的同一个人的人脸图片。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QiYfafDT-1571978683685)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H456X6pQ-1571978683685)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UIbmt5i-1571978683686)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
处理过程见附录代码
处理后的训练集
处理后的图片不再根据人种和不同人种的人进行分文件夹,其中所有而是将所有的图片分为训练集和验证集,这里并没有做local cv(本地交叉验证)。所以这里主要关注训练集,训练集中共有285346张图片,其中对于不同的人,可以通过文件名进行区分,在文件名中,“_”之前的代表对人的编号,之后的是这个人的第几张图片,所以后面的label也是采用的文件名中的前半部分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JiDYXVNY-1571978683686)(C:\Users\liuyi\AppData\Roaming\Typora\typora-user-images\image-20191024195940597.png)]
3.2 模型
应用的总体模型是insightface paper 。
InsightFace 是一个 2D/3D 人脸分析项目。InsightFace的代码是在MIT许可下发布的。 对于acadmic和商业用途没有限制。包含注释的训练数据(以及使用这些数据训练的模型)仅可用于非商业研究目的。在此存储库中,我们提供用于深度识别的训练数据,网络设置和损失设计。 训练数据包括标准化的MS1M,VGG2和CASIA-Webface数据集,这些数据集已经以MXNet二进制格式打包。 网络主干包括ResNet,MobilefaceNet,MobileNet,InceptionResNet_v2,DenseNet,DPN。 损失函数包括Softmax,SphereFace,CosineFace,ArcFace和Triplet(Euclidean / Angular)Loss。
ArcFace最初在arXiv技术报告中描述。 通过使用此存储库,用户可以通过单个模型简单地实现LFW 99.80%+和Megaface 98%+。 该存储库可以帮助研究人员/工程师通过两个步骤快速开发深度识别算法:下载二进制数据集并运行训练脚本。
3.2.1 网络结构
网络 | 简述 |
---|---|
ResNet | ResNet作为一种十分通用的网络,主要是利用一种shortcut的结构 |
MobileNet | 一种轻量级网络,也常常用于人脸识别 |
代码中关于网络部分的选择,其中args参数存储在utils类中
if checkpoint is None:
if args.network == 'r18':
model = resnet18(args)
elif args.network == 'r34':
model = resnet34(args)
elif args.network == 'r50':
model = resnet50(args)
elif args.network == 'r101':
model = resnet101(args)
elif args.network == 'r152':
model = resnet152(args)
elif args.network == 'mobile':
model = MobileNet(1.0)
else:
model = resnet_face18(args.use_se)
3.2.2 损失函数
在本项目中含有两类损失,因为是要衡量两种人脸的相似度,所以有一个用来衡量相似度的损失,这里直接调用的是ArcFaceloss,另一个loss则就是用来进行训练的loss,这里调用的是focalLoss。
第一类损失
- Softmax
损失函数是另一个提升识别精度的关键工作,在大家极力压榨网络骨干结构换取性能提升的时候,回头再来看损失函数这个网络训练的指挥棒,会有更多的发现。做为最常见的分类损失 Softmax,其定义如下:
Softmax 是最常见的人脸识别损失函数,然而,Softmax 不会显式的优化类间和类内距离的,所以通常不会有太好的性能。 - Triplet Loss
Triplet Loss 作为一种 Metric Learning,也在人脸识别中广泛使用。相比 Softmax,**其特点是可以方便训练大规模 ID(百万,千万)的数据集,不受显存的限制。**但是相应的,因为它的关注点过于局部,使得性能无法达到最佳且训练需要的周期非常长。 - SphereFace[2]
由于 Tripelet Loss 训练困难,人们逐渐寻求结合 Metric Learning思想来改进 Softmax 的思路,其中 SphereFace[2] 是其中的佼佼者,作为 17 年提出的一种方法,在当时达到了 state-of-the-art。在具体实现中,为了避免加入的 margin(即 m) 过大,引入了新的超参 λ,和 Softmax 一起联合训练。 - Additive Cosine Margin[3]
最近,在 [3],[4] 中,作者提出了一种在 Cosine 值上加入 Margin 的策略,加m而不是乘m的目的是为了方便求导。
模型获得了比 [2] 更好的性能,同时实现很方便,也摆脱了和 Softmax 联合训练的困扰,在训练起始阶段不再有收敛方面的问题。 - Additive Angular Margin[4]
我们提出了在角度上增加固定值的 Margin,在 Cosine Margin 的基础上,更具有几何 (角度) 解释性并且获得了更好的性能,这里我们同时 normalize 了 weight(到 1) 和 feature(到 s,默认 64),则 (Cosine Margin 也同理),这种改进相对于Cosine margin的好处在于角度距离比余弦距离对于角度的影响更加猛烈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gmGczdlg-1571978683686)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxg4ABEc-1571978683688)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
调用的衡量图像相似程度的函数是ArcFace
metric_fc = ArcMarginModel(args)
第二类损失
如下是未变化前的交叉熵(cross entropy) loss,以二分类为例:
通过实验发现,即使是easy examples(Pt >>
- 改进1
因此,对于大量的easy negative examples,这些loss会主导梯度下降的方向,淹没少量的正样本的影响。所以,我们要降低easy example的影响,可以在他的前面加上一项权重,其中权重因子的大小一般为相反类的比重。即负样本不是多吗,它越多,我们给它的权重越小。这样就可以降低负样本的影响。
- 改进2
上面只是解决了正负样本的不平衡,没有解决easy和hard examples之间的不平衡,因此针对easy和hard样本,定义focal loss。这样,当对于简单样本,Pt会比较大,所以权重自然减小了。针对hard example,Pt比较小,则权重比较大,让网络倾向于利用这样的样本来进行参数的更新。
且这个权重是动态变化的,如果复杂的样本逐渐变得好分,则它的影响也会逐渐的下降。
focalloss的调用如下
# Loss function
if args.focal_loss:
criterion = FocalLoss(gamma=args.gamma).to(device)
else:
criterion = nn.CrossEntropyLoss().to(device)
focalloss的具体实现
##这里定义一种新的损失
class FocalLoss(nn.Module):
def __init__(self, gamma=0):
super(FocalLoss, self).__init__()
self.gamma = gamma
self.ce = torch.nn.CrossEntropyLoss()
def forward(self, input, target):
logp = self.ce(input, target)
p = torch.exp(-logp)
loss = (1 - p) ** self.gamma * logp
return loss.mean()
3.2.3 优化器
这里使用的优化器的特点是,根据训练步数来进行调整学习率,分别在第60000步和100000步来对学习率进行除以10的操作,这样做是为了让其在前期能够快速训练,到后期减慢训练速度。
class InsightFaceOptimizer(object):
def __init__(self, optimizer):
self.optimizer = optimizer
self.step_num = 0
self.lr = 0.1
def zero_grad(self):
self.optimizer.zero_grad()
def step(self):
self._update_lr()
self.optimizer.step()
def _update_lr(self):
self.step_num += 1
# divide the learning rate at 100K,160K iterations
if self.step_num == 60000 or self.step_num == 100000:
self.lr = self.lr / 10
for param_group in self.optimizer.param_groups:
param_group['lr'] = self.lr
def clip_gradient(self, grad_clip):
_clip_gradient(self.optimizer, grad_clip)
def adjust_learning_rate(self, new_lr):
for param_group in self.optimizer.param_groups:
param_group['lr'] = new_lr
print("The new learning rate is %f\n" % (self.optimizer.param_groups[0]['lr'],))
3.2.5 数据预处理和加载
在一般的模型中,尤其是对于图像问题,都需要对数据进行预处理,对于普通数据,预处理包含有填充缺失值,去掉异常值,对分类数据进行处理,数据分箱等等。而在图像处理中,自带了许多数据增强的办法,并通过一个Sequential函数将所有方法串联在一起。
本次实验我采取了图像中常用的数据处理方法,也就是先调整亮度,依概率进行水平翻转,将图像数据转换成tensor格式,进行标准化。
self.transforms = T.Compose([
T.Resize(self.input_shape[1:]),
# T.RandomCrop(self.input_shape[1:]),
T.ColorJitter(brightness=0.125, contrast=0.125, saturation=0.125),
T.RandomHorizontalFlip(),
T.ToTensor(),
T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
3.2.6 GPU运行和并行计算
pytorch具有设备无关的特性,可以理解为无论在什么设备上都可以运行自己写的代码。而to方法Tensors和Modules可以用于容易的将对象移动到不同的设备。to方法一般施加在数据和模型上
另外,如果拥有多GPU,可以使用nn.DataParallel使得模型进行并行运行,在不同gpu上都分配一个batch中的一部分数据。一般施加在模型上。
将模型加载到多GPU
model = nn.DataParallel(model,device_ids=[1])
metric_fc = nn.DataParallel(metric_fc,device_ids=[1])
数据实例进行加载到GPU
img = img.to(device)
label = label.to(device) # [N, 1]
3.2.7 模型训练流程
for i, (img, label) in enumerate(train_loader):
# Move to GPU, if available
img = img.to(device)
label = label.to(device) # [N, 1]
# Forward prop.
feature = model(img) # embedding => [N, 512]
output = metric_fc(feature, label) # class_id_out => [N, 10575]
# Calculate loss
loss = criterion(output, label)
# Back prop.
optimizer.zero_grad()
loss.backward()
# Clip gradients
optimizer.clip_gradient(grad_clip)
# Update weights
optimizer.step()
# Keep track of metrics
losses.update(loss.item())
top1_accuracy = accuracy(output, label, 1)
top1_accs.update(top1_accuracy)
3.3 模型关键点
3.3.1 BN层的使用
BN层的优点主要有以下几点:
- 加快训练速度,这样我们就可以使用较大的学习率来训练网络
- 提高网络的泛化能力,这样就可以减少dropout和正则化的使用
- BN层本质上是一个归一化网络层,可以替代局部响应归一化层(LRN层)。
- 可以打乱样本训练顺序(这样就不可能出现同一张照片被多次选择用来训练)论文中提到可以提高1%的精度。
BN层的位置:
BN层一般位于激活层之前(目的是为了让激活层的值更加稳定),在FC层或ConV层之后
BN层如果放在全连接层后面,则是对全连接层里面每个batch的属性进行计算;如果放在卷积层之后,则一般对channel进行,在tensorflow中可以指定,对哪一维(一般情况不出意外都是axis=channel),而在Pytroch是不用指定可以默认进行的。
具体实现:
out = self.conv1(out)
out = self.bn1(out)
out = self.prelu(out)
3.3.2 PyTorch中的模型保存和加载
如今,迁移学习的思想在各种领域都有很多应用,而在人脸识别领域也不例外,使用迁移学习,这里只说最基本的迁移学习方法,用别人与训练好的best_checkpoint加载进自己的模型,往往可以在短时间内使得自己的模型取得十分好的效果
Torch中加载当前模型的方法
具体可参考下列链接 https://www.sohu.com/a/137839632_717210
指令 | 解释 |
---|---|
torch.load | 比较通用的一种方法,既可以加载整个模型,也可以只加载参数,也可以加载其他的东西 |
net.load_state_dict | 只加载参数,他的参数可以为torch.load也可以为model_zoo.load_url |
torchvision.load | 最简单易用的一种模型 |
model_zoo.load_url | 根据model_zoo进行加载 |
torchvision.models | 自带的模型 |
另外,pytorch还提供了灵活的预加载方法,可以自己大调或微调模型,原来的预训练模型仍然可以使用。
模型保存的具体实现,
def save_checkpoint(epoch, epochs_since_improvement, model, metric_fc, optimizer, acc, is_best=False):
state = {'epoch': epoch,
'epochs_since_improvement': epochs_since_improvement,
'acc': acc,
'model': model,
'metric_fc': metric_fc,
'optimizer': optimizer}
# filename = 'checkpoint_' + str(epoch) + '_' + str(loss) + '.tar'
filename = str(epoch) + 'checkpoint.tar'
torch.save(state, filename)
if is_best:
torch.save(state, 'BEST_checkpoint.tar')
模型加载的具体实现
else:
checkpoint = torch.load(checkpoint)
#这里还需要自己加载进去
start_epoch = checkpoint['epoch'] + 1
epochs_since_improvement = checkpoint['epochs_since_improvement']
model = checkpoint['model']
metric_fc = checkpoint['metric_fc']
optimizer = checkpoint['optimizer']
4.实验结果
一共采用了6个模型,分别是ResNet18,ResNet34,ResNet50,ResNet101,ResNet152,MobileNet
对六种模型都分别采用了使用预训练模型和不使用预训练模型进行测试
结果发现由于网络结构较为复杂,如果不使用预训练模型,以ResNet50为例,大约需要训练20个小时才达到较好的效果,下图是训练三小时的情况:
如果使用预训练模型,所有模型都在1小时之内获得相当好的效果,并将训练好的数据进行提交分别获得如下结果
模型 | 得分 |
---|---|
ResNet18 | 0.52240216000 |
ResNet34 | 0.58140000000 |
ResNet50 | 0.60012350000 |
ResNet101 | 0.63840026000 |
ResNet152 | 0.62130296000 |
MobileNet | 0.60356296000 |
可得模型并不是越复杂结果越好,在ResNet上取得了最好的效果。
5.实验总结
5.1 对于Pytorch的总结
经过本次数据库新技术课的学习,了解了Pytorch这个原来从来没有体验过的dl框架,因为原来自己用TensorFlow比较多,刚开始接触Pytorch就深深体会到Pytorch相对于TensorFlow更加简单
1.我认为他最大的优点就是建立的神经网络是动态的,可以实时进行输出,而这一点在静态的TensorFlow上(2.0已经支持了eager模式)并没有很好地体现。
2.Pytorch另外一个十分特别的地方就是自动求导机制,这点起码在我自己感觉在实现上与TensorFlow有很大的差别,对于每个batch的训练都要经过梯度清零,反向传播,迭代优化的过程。
3.另外,Pytorch的设备无关性对于GPU和多GPU执行十分方便,可以十分轻松的将模型迁移到GPU上或迁移到多GPU上执行。
4.Pytorch在模型加载方面也做得十分好,可以十分方便的加载别人预训练好的模型或者自己训练到一半的模型。
5.2 对于人脸识别的总结
由于原来并没有十分仔细的了解人脸识别这个CV中已经落地十分成功的方向,所以趁着本次数据库作业,也对人脸识别进行了一定的了解。
1.CV中对于人脸的处理有很多应用,例如人脸识别,人脸检测等等
2.人脸识别领域,目前常用的数据集主要是LFW和megaface,前者是一个baseline数据集,很多算法在这个数据集上的准确率已经超过了百分之99,而megaface数据集就完全不同,他是一个十分大的人脸数据集,很多在lfw上达到很好效果的模型在其上面只取得了60%左右的准确率
3,目前人脸识别领域比较优秀的模型应该是insightface,它包含了模型(网络),损失函数,优化器等一系列内容。
引用
[1] Deng J, Guo J, Xue N, et al. Arcface: Additive angular margin loss for deep face recognition[C] //Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019: 4690-4699.
[2] Lin T Y, Goyal P, Girshick R, et al. Focal loss for dense object detection[C]//Proceedings of the IEEE international conference on computer vision. 2017: 2980-2988.
[3] Sidel P H, Mayhew G E. The emergence of context: a survey of mobilenet user behavior[J]. Niigata: International University of Japan, Yamato-machi, Minami Uonuma-gun, 2003.
附录
模型实现代码
class IRBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, use_se=True):
super(IRBlock, self).__init__()
self.bn0 = nn.BatchNorm2d(inplanes)
self.conv1 = conv3x3(inplanes, inplanes)
self.bn1 = nn.BatchNorm2d(inplanes)
self.prelu = nn.PReLU()
self.conv2 = conv3x3(inplanes, planes, stride)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
self.use_se = use_se
if self.use_se:
self.se = SEBlock(planes)
def forward(self, x):
residual = x
out = self.bn0(x)
out = self.conv1(out)
out = self.bn1(out)
out = self.prelu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.use_se:
out = self.se(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.prelu(out)
return out
数据集划分代码
#这个脚本的目的在于对原始的数据集进行处理,划分验证集
import os
import random
import shutil
import copy
import numpy as np
import pandas as pd
from tqdm import tqdm
root_path = "../train_data/"
#生成新的文件夹存储划分的数据
if os.path.exists(root_path + 'new_training'):
pass
else:
os.makedirs(root_path + 'new_training')
if os.path.exists(root_path + 'new_training/train'):
pass
else:
os.makedirs(root_path + 'new_training/train')
#============================================================================================
race_list = ['African', 'Caucasian', 'Asian', 'Indian']
id = 0 #对每个人种图片编写id
for race in race_list:
race_satlist = [] #每个race中图片个数统计
data_path = root_path + 'training/' + race
data_list = os.listdir(data_path) #这里是整个文件夹中的文件
data_len = len(data_list) #每个人种的人数
print(race, '有:', data_len) #统计每个人中的数量,也就是数据集的数量
if race == 'Caucasian':
val_num = int(data_len * 0.0)
else:
val_num = int(data_len * 0.0)
#还没有加local cv
val_list = random.sample(data_list, val_num)
train_list = list(set(data_list).difference(set(val_list)))
#复制制作训练集
for i in train_list:
pic_file = data_path + '/' + i
race_satlist.append(len(os.listdir(pic_file)))#每个人照片的数量
for ind, j in enumerate(os.listdir(pic_file)):
pic = pic_file + '/' + j #精确到图片
new_pic = root_path + 'new_training/train/' + str(id) + '_' + str(ind) + '.jpg'
shutil.copyfile(pic, new_pic) #复制图片
id += 1
train_list = list(set(data_list).difference(set(val_list)))
#复制制作训练集
for i in train_list:
pic_file = data_path + '/' + i
race_satlist.append(len(os.listdir(pic_file)))#每个人照片的数量
for ind, j in enumerate(os.listdir(pic_file)):
pic = pic_file + '/' + j #精确到图片
new_pic = root_path + 'new_training/train/' + str(id) + '_' + str(ind) + '.jpg'
shutil.copyfile(pic, new_pic) #复制图片
id += 1