解析如何手把手教你做人脸搜索系统

  • 人脸识别原理简要说明这一章节看得云里雾里的,请问数学基础对于人工智能领域的学习很重要吗?

  • 克隆 InsightFace 代码后,为什么还要进行源码修改?

  • InsightFace 算法关键代码剖析很难理解,有什么好的学习办法吗?

  • 你是怎么想到做这么一个人脸搜索系统的?

  • 我没有 GPU 可以训练人脸识别模型吗?

  • 这个模型是基于 Mxnet 的,为什么不使用现在非常火的 Tensorflow?

  • 使用这个别人训练好的模型,对硬件要求还大吗?

  • 模型需要什么基础吗?

  • 模型是基于什么语言做的?

  • 红外识别人脸的技术,关键的问题在哪里?

  • 可以对 Arcface 源码进行讲解吗?

  • 你的项目里,硬件配置是怎样的?

  • 我之前使用过 InsightFace 移植于 Android 使用,做过大部分 Python 测试,也借助学校机器尝试训练,看到这个 Chat 的时候主要的目的在于你的搜索系统,相反,人脸识别部分反而不是重点。能详细说说搜索这块的方法吗?

  • 目前开源的人脸检测算法有比 MTCNN 更好的吗?

  • 你是怎么学习深度学习的,对于深度学习入门有什么好的建议?


问:人脸识别原理简要说明这一章节看得云里雾里的,请问数学基础对于人工智能领域的学习很重要吗?

答:在我们这个 Chat,识别原理简要说明看不懂,不影响我们把人脸搜索系统做起来,因为我们是基于开源已经训练好的模型文件来进行系统搭建。这只是一个抛砖引玉的学习项目,我们不应该满足于此,对于想入门人工智能领域的同学来说,看懂基础的数学公式是很重要的。没有数学基础,就没办法理解这个模型网络为什么要这么搭建,损失函数为什么要这么设计。有想深入学习的同学,我建议大家把开源项目 InsightFace 论文及源码都好好捋一遍,哪里不懂补哪里,这样带着项目,带着问题去学,会事半功倍的。


问:克隆 InsightFace 代码后,为什么还要进行源码修改?

答:因为 InsightFace 作者的运行环境是 Python 2.7,我们这个项目的运行环境是 Python 3.5,这两个 Python 版本有些语法是不同的,所以要进行适当修改。现在大家使用 Python 3.x 还是多一些,所以碰到使用 Python 2.x 的代码,我都会改成 3.x。只是使用习惯问题。如果大家使用 Python 2.x,是不需要修改的。


问:InsightFace 算法关键代码剖析很难理解,有什么好的学习办法吗?

答:我们学习要看懂这段代码,首先我们必须有一定的深度学习基础,比如神经网络是怎么工作的,对输入层、隐藏层、输出层、全连接、神经元、卷积等这些神经网络概念要有一定的理解。然后就是对深度学习框架(如 Mxnet,Tensorflow)的工作原理也要有一些了解,比如什么是网络计算图,什么是反向传播等。

这些都理解了,接下来才是代码的学习,我觉得相比原理、概念的学习,代码的学习倒是比较简单。因为别人代码都写出来了,你就按着代码一步一步执行,看看输入是什么,输出是什么,之间有什么关系,再加上查找下官方文档说明及示例,基本上就知道为什么代码要这么写了。

我接下来在准备一个 Chat,讲的是程序员的第一性原理,基于这个第一性原理(暂时保密),我给出了几种学习方法(如“断联法”、“去元法”等)。然后再用程序代码做为例子来讲如何应用这些方法,其中就有一个例子是用 InsightFace 算法关键代码来讲解其中一种学习方法的。

 


问:你是怎么想到做这么一个人脸搜索系统的?

答:这个是纯粹自己有兴趣,自己做来自己玩的。百度云产品有个人脸识别,可以进行人脸检测,人脸比对,人脸搜索。

 

 

我试用了下这些功能,觉得很炫,所以想动手自己做一个,于是就有了这个项目。当然这个项目是比较简单的,是用了别人训练好的模型文件。我也有自己训练模型,但训练过程比较麻烦一些,就没放到 Chat 上来讲。Chat 上讲的都是我自己实践,并且调试、验证出来的。把自己所学写成文章,对自己也是有很大好处的,所以就有了这么一个小项目。


问:我没有 GPU 可以训练人脸识别模型吗?

答:要训练出一个好的人脸识别模型,需要非常大的训练数据,几百万张图片,几十个周期的迭代训练。如果没有 GPU 的话,只使用 CPU,估计要训练一个月。所以在深度学习时代,GPU 的重要性越来越明显了。没有 GPU,只能训练一些小数据量的玩具模型。


问:这个模型是基于 Mxnet 的,为什么不使用现在非常火的 Tensorflow?

答: Insightface 作者选择使用 Mxnet,所以大部分人也就跟着作者使用 Mxnet。至于为什么不使用 Tensoflow,我觉得是个人使用习惯的问题。对于学习来说,选择什么样的开源框架,倒不是很重要。重要的是学习模型的设计原理,训练原理,以及深度学习的工作原理。每个框架在这些原理方面,其实都是共通的,学会了 Mxnet,再去学 Tensorflow,也是很容易上手的,所谓一通百通。当然,如果是生产项目选型的话,则是另一回事了,要考虑很多工程因素,这里就不展开说了。


问:使用这个别人训练好的模型,对硬件要求还大吗?

答:我们这个是使用别人训练好的模型文件,对硬件的要求不大,普通的笔记本电脑(如 i5 处理,8G 内存)就可以跑起来。


问:模型需要什么基础吗?

答:基于别人训练好的模型文件,搭建人脸搜索系统,只要 Linux、Python、Nginx、Docker 基础即可。当然,即使这些掌握的一般般,按照文章的步骤操作,也可以搭建起来。


问:模型是基于什么语言做的?

答:使用 Python 语言,现在很多深度学习框架都配备有主流的 Python、R、Java 接口,现在使用 Python 的人还是比较多的。


问:红外识别人脸的技术,关键的问题在哪里?

答:这个不是红外识别技术,这个是一般的人脸图片,经过神经网络之后,提取到人脸特征编码。然后基于这些人脸特征编码进行比对,计算得出这些编码的距离、相似度,以此来鉴定是不是同一个人。

要说关键的问题,当然就是特征编码的好坏了。好的特征编码,然后非常好的区分同一个和不同个人,能够最大化类间距离、最小化类内距离。比如同一个的相似度都能在 80% 以上,不同个人的相似度都能在 50% 以上,中间的模糊地带越少越好。这样就不容易混乱,比如相似度在 70%,到底是同个人,还是不同个人,很难说。所以 InsightFace 的关键作用就是训练出一个模型,他能很好的提到到人脸特征编码。我们基于这些编码,去做的人脸搜索系统。


问:可以对 Arcface 源码进行讲解吗?

答:大家打开 recognition 这个目录下的 train.py 文件,我们重点讲下 get_symbol 这个函数。

 

首先,大家要知道这个函数的作用是什么。这个函数的作用就是生成模型训练的网络计算图,不知道大家对网络计算图这个概念有没有理解。就是我们要训练模型,首先要定义一个网络计算图,比如简单举个例子,你要计算 c=a+b,这是最简单的一张图,里面有两个变量,一个操作符。在神经网络里,定义网络计算图,就包括神经层、卷积、池化、全连接、正则化、softmax 等等这些操作。

就是定义一张网络,然后模型就可以基于这张网络传入数据,forward 进行参数计算,最后得到输出值,输出值再与我们定义的标签进行比对,看网络计算的值与我们的标签值相差多少,然后就有一个 backward 反向传播的过程。反向传播的依据是什么?如果输出值与标签值对比,大了,我们下次就要调小这个输出值。小了,我们就要调大这个输出值。通过什么来调整输出值?就是通过网络中的参数 weight,而 weight 的调整就是基于反向传播的梯度下降。如果上面这些大家听了能理解的话,我们就可以进入下面的代码讲解了。

embedding = eval(config.net_name).get_symbol()

这一句的作用是得到 resnet 网络的最后一层 fc1 的输出值。resnet 是什么?就是卷积网络中最常用的残差网络。得到的这个 embedding 就是我们之前说的人脸特征编码,512 维的。

config.net_name 配置的是 ‘fresnet’,eval('fresnet').get_symbol() 等效于 fresnet.get_symbol()。那这里为什么要用 eval,因为作者的源码里面不只有 resnet 这一种卷积网络,还有 mobilnet 等等网络,所以需要动态配置传入,才用到 eval 这个可以动态生成的功能。声明一下,我不会把源码中的每行代码都拿来讲,里面很多实现是其他损失函数,我们只讲 Arcface 这一种。

all_label = mx.symbol.Variable('softmax_label')

定义一个符号变量作为输入的占位符,运行计算图时会有数据填充之。在 image_iter.py 有定义 label_name='softmax_label'。softmax_label 在 model.fit() 的时候进行变量绑定 bind(),将数据集输入的数据绑定到变量上。还有一个变量是 data, 在 fresnet.py data = mx.sym.Variable(name='data') 进行绑定。

model.fit 里面有进行模型绑定操作。

self.bind(data_shapes=train_data.provide_data, label_shapes=train_data.provide_label,

for_training=True, force_rebind=force_rebind)

绑定的 provide_data 为 [('data', (100, 3, 112, 112))],绑定的 provide_label 为 [('softmax_label', (100,))]。train_dataiter.next() 返回 return io.DataBatch([batch_data], [batch_label], batch_size - i)。

gt_label = all_label

_weight = mx.symbol.Variable("fc7_weight", shape=(config.num_classes, config.emb_size),

lr_mult=config.fc7_lr_mult, wd_mult=config.fc7_wd_mult, init=mx.init.Normal(0.01))

mx.init.Normal(0.01) 用从正态分布中采样的随机值初始化权重,其中均值为零,西格玛标准差。参数:sigma(float,optional) - 正态分布的标准偏差。 默认标准偏差为 0.01。这个_weight 就是在定义全连接的权重,并且使用正态分布数值初始化它。

s = config.loss_s # 64

_weight = mx.symbol.L2Normalization(_weight, mode='instance')

对 w 进行归一化,使每个样本的范数为 1,既然每个样本的范数为 1,那么样本的每个元素的范围也不会超过 1,也就在 [-1,1] 范围内。mode='instance' 表示对每个样本进行正则化。

nembedding = mx.symbol.L2Normalization(embedding, mode='instance', name='fc1n')*s

对 x 进行归一化,并得到 s*x,(B,F), B=Batch,F=Feature,即(100,512)。

fc7 = mx.sym.FullyConnected(data=nembedding, weight = _weight, no_bias = True, num_hidden=config.num_classes, name='fc7')

这个就是全连接,就是在 resnet 的输出之后接一个全连接层。全连接的作用就是用线性回归去拟合特征编码 Y=wx+b。Y=XW'+b,(B,F)*(C,F)'=(B,C),' 为转置,此处得到 s*cos(theta) (100,85742)。输出的数据,类似这样:

# [[0.1,-0.3,0.4 ... 0.2] ... [...]] shape=(100,85742)

我上面都假设批大小,即 Batch=100。

zy = mx.sym.pick(fc7, gt_label, axis=1)

得到 fc7 中 gt_label 位置(如第 1456 类)的值。(B,1) 或者 (B),即当前 batch 中 yi 处的 scos(theta),得到的输出类似这样:

[0.3,0.2,0.6 ... 0.4]*64 shape=(100,)

cos_t = zy/s

由于 fc7 及 zy 均为 cos 的 s 倍,此处除以 s,得到实际的 cos 值。(B,1) 或者 (B),输出的数据类似这样:

[0.3,0.2,0.6 ... 0.4] shape=(100,)

t = mx.sym.arccos(cos_t)

arccos 表示的是反三角函数中的反余弦。输出的数据类似这样:

[20,30,38,14 ... 76] shape=(100,)

t = t+config.loss_m2

这一句是重点,就是作者提出的 arcface 损失函数的关键步骤。就是在 W 和 X 的角度上加上一个 m。以此,可以使得类内距离更小,类间距离更大。cosx 是不是一个递减函数,角度越大,cos 值就越小。比如我们这个样本是属于第 50 类,那么它所占的比重就是第 50 类的输出值/所有类的输出值。我们通过加大角度降低第 50 类的输出值,然后还要求他所占的比重最大,这样是不是就能更好的区分类别。

body = mx.sym.cos(t)

输出类似这样:

[0.28,0.22,0.56 ... 0.38] shape=(100,)

new_zy = body*s

输出类似这样:

[0.28,0.22,0.56 ... 0.38]*64 shape=(100,)

diff = new_zy - zy

这个 diff 就是增大角度之后的余弦值跟原先的余弦值的差值。

diff = mx.sym.expand_dims(diff, 1)

这个是扩展维度,为了跟后面的函数使用匹配上。输出的数据,类似这样:

[[0.01],[0.02],[0.06] ... [0.03]] shape=(100,1)

gt_one_hot = mx.sym.one_hot(gt_label, depth = config.num_classes, on_value = 1.0, off_value = 0.0)

one_hot 就是独热编码,就是一个向量之中,只有一位是 1,其他位都是 0。输出数据,类似这样:

[[0,0 ... 0,1 ...] ... [0,0 ... 1,0 ...]] shape=(100,85742)

body = mx.sym.broadcast_mul(gt_one_hot, diff)

输出数据类似这样:

[[0,0 ... 0,0.01 ...] ... [0,0 ... 0.06,0 ...]] shape=(100,85742)

fc7 = fc7+body

对应 yi 处,fc7=zy+diff=new_zy,其他处不变。输出数据类似这样:

[[0.1,-0.3 ... 0.406 ... 0.2] ... [...]] shape=(100,85742)

out_list = [mx.symbol.BlockGrad(embedding)]

阻塞 embedding 节点反向传播,不计算梯度。用于 verification.test,注意下 out_list 的作用。model.forward(db, is_train=False) 时,model.get_outputs() 只会得到 embedding,不会得到 softmax。model.forward(db, is_train=True) 时,model.get_outputs() 既会得到 embedding,也会得到 softmax,大家注意下上面这两种情况的区别,也体会下 BlockGrad 的作用。计算相对于 softmax 输出的交叉熵损失梯度,用于 model.update()、out_list.append(softmax)、out = mx.symbol.Group(out_list)、return out、model.get_outputs() 会得到 out_list 里面定义的网络节点的输出值。

整个流程下来,就是在定义网络计算图。网络计算图定义好之后,就可以创建模型了。

sym = get_symbol(args)

sym 就是网络计算图。

model = mx.mod.Module(

context = ctx,

symbol = sym,

)

ctx 就是资源,如使用 GPU 还是 CPU。三驾马车,现在已经准备齐了两辆,网络计算图、模型,还差一辆是什么?数据。所以我们要定义一个数据集迭代器。

 

train_dataiter 就是一个 io 操作,可以批量获取训练数据。这三驾马车都准备好了之后,就可以训练模型了。


问:你的项目里,硬件配置是怎样的?

答:就这个项目而言,我是使用 Dell 笔记本,i5 CPU,8G 内存。


问:我之前使用过 InsightFace 移植于 Android 使用,做过大部分 Python 测试,也借助学校机器尝试训练,看到这个 Chat 的时候主要的目的在于你的搜索系统,相反,人脸识别部分反而不是重点。能详细说说搜索这块的方法吗?

答:搜索这块,我现在是用开源的搜索项目,就是 Annoy。但我深入了解了一下 Annoy,发现他很多功能都没办法满足要求。后来,我又了解到了,有另一个更强的搜索开源项目,就是 Facebook 的 Faiss。这个 Faiss,可以把索引保存到硬盘,也可以从硬盘加载到内存。而且,它还支持动态的 add 特征向量到 index 里面,也还动态的 remove,比起 Annoy,功能齐备上完胜。在性能上,Faiss 支持千亿级别的向量搜索,绝对是 Annoy 望尘莫及的。


问:目前开源的人脸检测算法有比 MTCNN 更好的吗?

答:目前开源的人脸检测算法,也有使用 YOLO 进行检测的,但效果还是没办法和 MTCNN 比。目前就我所知,MTCNN 的检测效果算最好的了。


问:你是怎么学习深度学习的,对于深度学习入门有什么好的建议?

答:在入门深度学习之前,你首先要问自己一个问题,我是否对基础研究有兴趣?还是只喜欢写上层应用代码?因为深度学习兴起,也就是这几年内的事情,从事的人员还不是很多。现在很多人都是从 Java,C++,数据库工程师转型过来的,这些工程师一般都有好些年的工作经验,都是写惯了 Java、C++、SQL 代码的人。突然转型到深度学习领域,会发现这是个多种学科交叉的领域。需要数学基础、统计学、Python、机器学习、神经网络深度学习等等知识,不知道从哪里下手是很正常的。

所以对于写惯上层应用的代码的人,可能一时半会儿,很难适应要去做很多基础理论研究,比如看论文,看数学推导公式,还要看代码。如果有充足的时间,我还是建议把理论基础学扎实了,买几本机器学习、深度学习的书好好看。如果有充足的时间,我还是建议把理论基础学扎实了,买几本机器学习、深度学习的书好好看。如果有充足的时间,我还是建议把理论基础学扎实了,买几本机器学习、深度学习的书好好看。如果没有时间的话,在网易云课程上有个吴恩达的课程,有讲机器学习,也有讲深度学习,卷积神经网络的,讲得非常好,非常适合刚入门的人去学。然后再加上开源项目的磨练,也能入门。另外,我觉得有一点很重要,跟大家分享,大家真的千万不要胡乱学习一通,这样很容易打击信心。大家要停下来,想想怎么学习的问题。学错了方向,真的是白费力。所以先想清楚自己的学习能力,学习方法。然后再去学习,找到方向,刻意练习,这样才是有效果的。

关于 Python 的学习,有一个工具,可能大家也都知道。就是 Jupyter Notebook。上面可以一行一行的执行代码,调试代码,非常直观和方便。我自己就买了好几本书,给大家看下书单:

 

这些书能补充自己的基础功。我觉得很重要。特别是《深度学习》这本书,被称为机器学习界的圣经。里面大而全,如果能静下心来好好研究的话,一定会有很大的收获。但这本书不适合刚入门的人去学,因为容易打击信心。所以大家要调整好心态,这个很重要。刚入门的话,还是以兴趣为主,做些简单的项目,然后项目中遇到问题去查、去解决。有点感觉了,再去啃这些比较难懂的书。

posted @ 2020-12-22 20:07  李传陆  阅读(664)  评论(0编辑  收藏  举报