写给程序员的机器学习入门 (十三) - 人脸识别(一)


风晓
风晓 2023-12-31 10:13:22 55610 赞同 0 反对 0
分类: 资源
这篇将会介绍人脸识别模型的实现,以及如何结合前几篇文章的模型来识别图片上的人

实现人脸识别的方法

你可能会想起第八篇文章介绍如何识别图片上物体类型的 CNN 模型,那么人脸是否也能用同样的方法识别呢?例如有 100 个人,把这 100 个人当作 100 个分类,然后用他们的照片来训练,似乎就可以训练出可以根据图片识别哪个人的模型了,真的吗🤔。

很遗憾,用于识别物体类型的模型并不能用在人脸识别上,主要有以下原因:

  • 识别物体类型的模型通常要求每个分类有大量的图片,而人脸识别模型很多时候只能拿到个位数的人脸,这样训练出来的精度很不理想。这个问题又称 One-shot 学习问题 (每个分类只有很少的样本数量)。
  • 识别物体类型的模型只能识别训练过的类型,如果想添加新类型则需要重新开始训练 (如果一开始预留有多的分类数量可以基于上一次的模型状态继续训练,这个做法又称迁移学习)
  • 同上,识别物体类型的模型不能识别没有学习过的人物

我们需要用不同的方法来实现人脸识别😤,目前主流的方法有两种,一种是基于指标,根据人脸生成对应的编码,然后调整编码之间的距离 (同一个人的编码接近,不同的人的编码远离) 来实现人脸的区分;另一种是基于分类,可以看作是识别物体类型的模型的改进版,同样会根据人脸生成对应的编码,但最后会添加一层输出分类的线性模型,来实现间接的调整编码。

基于指标的方法

基于指标的方法使用的模型结构如下:

我们最终想要模型根据人脸输出编码,如果是同一个人那么编码就会比较接近,如果是不同的人那么编码就会比较远离。如果训练成功,我们可以根据已有的人脸构建一个编码数据库,识别新的人脸时生成新的人脸的编码,然后对比数据库中的编码找出最接近的人脸,如下图所示。

输出编码的模型定义如下,这里的编码长度是 32 (完整代码会在后面给出):

# Resnet 的实现
self.resnet = torchvision.models.resnet18(num_classes=256)
# 支持黑白图片
if USE_GRAYSCALE:
    self.resnet.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
# 最终输出编码的线性模型
# 因为 torchvision 的 resnet 最终会使用一个 Linear,这里省略掉第一个 Linear
self.encode_model = nn.Sequential(
    nn.ReLU(inplace=True),
    nn.Linear(256, 128),
    nn.ReLU(inplace=True),
    nn.Linear(128, 32))

而比较编码找出最接近的人脸可以使用以下的代码 (计算编码中各个值的相差的平方的合计):

diff = (new_code - exists_code).pow(2).sum(dim=1).sort()
most_similar = diff.indices[0]

如果编码数据库中有大量的编码,计算所有编码的距离消耗会比较大,我们可以使用支持搜索向量的数据库,例如把编码保存到 Elastic Search 数据库并使用 dense_vector 类型,Elastic Search 数据库会根据编码构建索引并实现更高效的查找🤒。

看到这里你可能会觉得,就这么点吗?那该如何训练模型,让同一个人的编码更接近呢?这就是最难的部分了🤕,一开始大家想到的是以下的方式:

# 计算损失的逻辑
loss1 = 同一个人的编码距离
loss2 = ReLU(常量A - 不同的人的编码距离)
loss = loss1 + loss2

这样做看上去经过训练以后同一个人的编码会完全相同,而不同的人的编码距离最少需要大于常量A,但实际上这个方式很难训练成功,因为即使是同一个人,图片上的人脸角度、光线、脸色、以及背景都不一样,生成完全一样的编码会非常困难。

2015 年的 Facenet 论文 提出了使用 Triplet Loss 来训练人脸识别模型的手法,简单来说就是同时准备两个人的三张图片 (又称三元组),然后让同一个人的编码距离小于不同的人的编码距离,计算方式如下:

loss = ReLU(同一个人的编码距离 + 常量A - 不同的人的编码距离)

看上去只是把前面的 loss1 放到了 loss2 的 ReLU 函数里面啊,对🤒,这么做了以后同一个人的编码距离不需要等于 0,只需要和不同的人的编码距离相差常量A即可,如下图所示:

经过训练以后的编码分布大概会像下图,同一个人物的编码不会完全一样但会聚集在一起 (这就是一种通过机器学习实现聚类的方法🤒):

现在我们知道选取两个人的三张图片 (又称三元组),然后使用 Triplet Loss 计算损失即可训练模型聚类人脸,那应该怎样选取图片呢?简单的做法是随机选取图片,但随着训练次数增多,同一个人物的编码距离小于不同人物的编码距离的频率就越高,也即是说 loss 为 0 的频率越高,如果 90% 的 loss 为 0,那么就代表 90% 的计算都白费了。而且,这样训练出来的模型对于看上去相似但是不是同一个人的识别能力会比较弱。

更好的方法是记录上一次训练时各个图片的编码,先选取一张基础图片 (Anchor),然后选取"同一个人物但编码距离最远"的一张图片 (Hard Positive),和"不同的人但编码距离最近"的一张图片 (Hard Negative)。这样可以给模型尽可能大的压力来训练看上去不相似但是同一个人,和看上去相似但不是同一个人的识别能力。这个方法实现起来有一定的难度,因为:

  • 如果训练的图片数量很多,例如上百万张,那么每次选取图片都需要计算基础图片的编码和上百万个编码之间的距离,计算量会非常庞大,训练起来像乌龟一样慢😱
  • 如果你不小心把同一个人的图片放到其他人的文件夹,或者混杂一些质量比较垃圾的图片,这时候就好玩了,模型会想方设法的去适应这些图片,导致训练出来的模型不能泛化
  • 如果你一直给模型看很相似的人物然后告诉模型这不是同一个人 (有可能因为第二个原因,也有可能因为真是双胞胎🤗),那模型下次看到同一个人也会怀疑不是同一个,同样会影响模型的泛化能力

Facenet 中缓解这个问题的方法是把图片切分小批次 (Mini batch),然后在小批次中局部查找编码距离最近但不同的人,也会在小部分样本中随机选取批次外的人物。本文给出的实现将会使用另一种方法,具体看后面的介绍吧🥳。

顺道一提,Facenet 中使用的编码长度是 32,常量A的值是 0.2,本文的实现也会使用相同的参数,参考后面给出的代码叭。

基于分类的方法

基于指标的方法可以直接调整编码之间的距离,但选取三元组有一定的难度,并且随着数据量增多,选取时的计算量也会越多。基于分类的方法是另外一种途径,可以无须选取三元组而间接的调整编码之间的距离。模型的结构如下:

看起来只是在输出编码以后加一个单层线性模型,然后输出人物对应的分类,如果是同一个分类那么编码应该会更接近。如果忽视掉编码,把多层线性模型和单层线性连在一起,就是普通识别物体类型的模型。真的能行吗?

当然,没这么简单🤬,如文章开始提到过的,直接应用识别物体类型的模型到人脸上效果会很差,因为各个人的样本数量都不多,加上最后的单层线性模型只需要划分编码到分类而不需要聚集编码,训练出来的模型识别能力会很弱。训练出来的人脸分布可能会像下图一样:

关键点在于计算损失的函数,普通识别物体类型的模型会使用 Softmax + CrossEntropyLoss,而识别人脸的模型则需要使用变种函数计算损失,本文不会详细介绍这些变种函数,如果你有兴趣可以参考 CosFaceSphereFace,和 ArcFace 的论文。

因为基于分类的方法速度更快,它更适合计算数量非常庞大的数据集,而这篇的例子收集到的人脸数据比较少,所有还是会采用基于指标的方法来实现,慢一点就慢一点吧🤒。

关于计算编码距离的补充

计算编码距离主要有两种方法,第一种是计算欧几里德距离 (Euclidean Distance),也就是在前面看到过的计算方法;第二种是计算余弦相似度 (Cosine Similarity),如果你参考基于分类的方法的论文会发现里面基本上都会使用余弦相似度计算。

使用 pytorch 计算欧几里德距离的例子如下,Triplet Loss 使用的时候会除掉 sqrt 的部分:

>>> import torch
>>> a = torch.tensor([1, 0, 0, 0.9, 0.1])
>>> b = torch.tensor([1, 0, 0.2, 0.8, 0])
>>> (a - b).pow(2).sum().sqrt()
tensor(0.2449)
// 结果等于 0 代表完全相同

使用 pytorch 计算余弦相似度的例子如下:

>>> import torch
>>> a = torch.tensor([1, 0, 0, 0.9, 0.1])
>>> b = torch.tensor([1, 0, 0.2, 0.8, 0])
>>> torch.nn.functional.cosine_similarity(a, b, dim=0)
tensor(0.9836)
// 相当于
>>> (a * b).sum() / (a.pow(2).sum().sqrt() * b.pow(2).sum().sqrt())
tensor(0.9836)
// 结果等于 1 代表完全相同,结果等于 0 代表完全相反

实现人脸认证的方法

使用以上的方法我们可以找到最接近的人脸,那这个人脸是否就是我们传入的人脸是同一个人呢?判断是否同一个人的依据可以是编码距离是否小于某个阈值,然而这个阈值是很难定义的。有个现象是,经过训练的同一个人的编码距离明显小于没有经过训练的同一个人的编码距离,人脸分布可能会如下:

一个比较好的方法是训练另外一个专门根据人脸编码距离判断是否同一个人的模型,这个模型只有一层线性模型,它会给编码中的每个指标乘以一个系数,然后加上偏移值,再交给 Sigmoid 转换到 0 ~ 1 之间的值,0 代表不是同一个人,1 代表是同一个人。

模型的定义如下,完整代码参考后面:

self.verify_model = nn.Sequential(
    nn.Linear(32, 1),
    nn.Sigmoid())

假设如果有以下的编码:

>>> a = torch.tensor([1, 0, 0, 0.9, 0.1])
>>> b = torch.tensor([1, 0, 0.2, 0.8, 0])
>>> c = torch.tensor([1, 1, 0.9, 0.2, 1])

a 与 b,a 与 c 之间的距离可以用以下方式计算:

>>> diff_1 = (a - b).pow(2)
>>> diff_1
tensor([0.0000, 0.0000, 0.0400, 0.0100, 0.0100])
>>> diff_2 = (a - c).pow(2)
>>> diff_2
tensor([0.0000, 1.0000, 0.8100, 0.4900, 0.8100])

再假设模型参数如下:

>>> w = torch.tensor([[-1.58, -2.96, -0.8, -0.1, -1.28]]).transpose(0, 1)
>>> b = torch.tensor(3.68)

再应用到编码相差值就会发现 a 与 b 是同一个人的可能性很高 (接近 1),而 a 与 c 是同一个人的可能性很低 (接近 0):

>>> torch.nn.functional.sigmoid(diff_1.unsqueeze(0).mm(w) + b)
tensor([[0.9743]])
>>> torch.nn.functional.sigmoid(diff_2.unsqueeze(0).mm(w) + b)
tensor([[0.2662]])

训练人脸认证模型的代码会在后面给出。

看到这里你可能会问,为什么需要给编码中的指标分别训练不同的系数呢?不能直接用 sum 相加起来,再根据这个相加的值来判断吗?想想编码里面的内容代表了什么,模型为了区分人脸,需要给不同的人物分配不同的编码,而这个编码实际上就隐含了人物的属性,例如某个指标可能代表人物的性别,某个指标可能代表人物的年龄,某个指标可能代表人物的器官形状,这些指标的相差值有的会更重要,例如代表性别的指标不一致那就肯定是不同的人了,而代表年龄的指标不一致则还有余地。给每个指标分别训练不同的系数 (也可以称为权重) 可以更精准的判断是否同一个人。

准备训练使用的数据集

好了,又到动手的时候了🤗。首先我们需要准备数据集,这次还是在 kaggle 上扒,一共有三个数据集符合要求,地址如下:

合计一共有 5855 个人和 33329 张图片,和其他公用数据集一样,里面大部分是白人,对亚洲人和黑人的效果会打个折🤒。训练人脸识别模型通常需要上百万张人脸,而这里只有三万多张,所以预计精确度会稍微低一些🤕。

需要注意的是,里面有相当一部分人物是只有一张图片的,这种人物会只拿来当负样本 (不同的人物) 使用。

此外,训练人脸识别模型的时候人脸的位置和占比要比较标准,数据质量会直接影响训练出来的正确率。以上三个数据集的人脸图片都是经过预处理的,不需要使用上一篇文章介绍的模型来调整中心点,但数据集的人脸占比不一样,所以会经过裁剪再参与训练。

裁剪比例分别是:

  • 第一个数据集:中心 50%
  • 第二个数据集:不裁剪
  • 第三个数据集:中心 70%

运行后面的代码以后可以到 debug_faces 目录下查看裁剪后的人脸,内容大致如下:

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南  2121
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访?  2026
银河麒麟桌面操作系统【保留数据盘重装系统】  1837
麒麟系统各种原因开不了机解决(合集)  1654
桌面通用(全架构)【rpm包转成deb包】操作方法  935
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题  920
统信系统安装(合集)  870
统信桌面专业版【手动分区安装UOS系统】介绍  852
统启动异常几种类型(initramfs 模式)  693
Linux系统软件包的导出  26
最近下载排行榜
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南 0
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访? 0
银河麒麟桌面操作系统【保留数据盘重装系统】 0
麒麟系统各种原因开不了机解决(合集) 0
桌面通用(全架构)【rpm包转成deb包】操作方法 0
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题 0
统信系统安装(合集) 0
统信桌面专业版【手动分区安装UOS系统】介绍 0
统启动异常几种类型(initramfs 模式) 0
Linux系统软件包的导出 0
作者收入月榜
1

prtyaa 收益393.72元

2

zlj141319 收益221.42元

3

1843880570 收益214.2元

4

IT-feng 收益213.03元

5

风晓 收益208.24元

6

777 收益172.82元

7

Fhawking 收益106.6元

8

信创来了 收益105.89元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.65元

请使用微信扫码

加入交流群

请使用微信扫一扫!