18

基于Caffe的DeepID2实现(上)

小喵的唠叨话:小喵最近在做人脸识别的工作,打算将汤晓鸥前辈的DeepID,DeepID2等算法进行实验和复现。DeepID的方法最简单,而DeepID2的实现却略微复杂,并且互联网上也没有比较好的资源。因此小喵在试验之后,确定了实验结果的正确性之后,才准备写这篇博客,分享给热爱Deep Learning的小伙伴们。

能够看到这篇博客的小伙伴们,相信已经对Deep Learning有了比较深入的了解。因此,小喵对亲作了如下的假定:

  1. 了解Deep Learning的基本知识
  2. 目前在做人脸识别的相关工作
  3. 仔细阅读过DeepID和DeepID2的论文
  4. 使用Caffe作为训练框架
  5. 即使不满足上述4个条件,也会持之以恒的学习

如果亲发现对上述的条件都不满足的话,那么这篇博文可能内容还是略显枯涩乏味,你可以从了解Caffe开始,慢慢学习。

相关资源:

由于篇幅较大,这里会分成几个部分,依次讲解。

一、设计我们独特的Data层

在DeepID2中,有两种监督信号。一是Identity signal,这和DeepID中的实现方法一样,用给定label的人脸数据,进行分类的训练,这里使用softmax_with_loss层来实现(softmax+cross-entropy loss)。这里不再介绍。

另一种就是verification signal,也就是人脸比对的监督。这里要求,输入的数据时成对存在,每一对都有一个公共的label,是否是同一个类别。如果是同一个identity,则要求他们的特征更接近,如果是不同的identity,则要求他们的特征尽可能远离。

不论最终怎么实现,我们的第一步是确定的,构造合适的数据。

使用Caffe训练的时候,第一步是打Batch,将训练数据写入LMDB或者LevelDB数据库中,训练的时候Caffe会从数据库中读取图片,因此一个简单的实现方法就是构造许多的pair,然后打Batch的时候就能保证每对图片都是相连的,然后在训练的时候做一些小Trick就可以实现。

但是就如上面所说,打Batch的同时,图片的顺序就已经是确定的了,因此网络输入的图片pair也是固定的,这样似乎就缺乏了一些灵活性。

那么如何动态的构造我们的训练数据呢?

设计我们独特的data层。

这里为了方便,使用Python来拓展Caffe的功能。Python是一门简洁的语言,非常适合做这种工作。不过Caffe中如果使用了Python的层,那么就不能使用多GPU了,这点需要注意(希望以后能增加这个支持)。

1)让你的Caffe支持Python拓展。

在Caffe根目录的Makefile.config中,有这么一句话。

with_python_layer

我们需要使用Python层,因此需要取消这个注释。

之后Make一下你的Caffe和pycaffe。

这样Caffe就支持Python层了。

2)编写data层

基于Python的data层的编写,Caffe是给了一个简单的例子的,在/caffe_home/examples/pycaffe/layers/中。

我们简单的照着这个例子来写。

首先,我们定义自己需要的参数。

这里,我们需要:

  • batch_size: batch的大小,没什么好解释的,要求这个数是大于0的偶数
  • mean_file:图像的均值文件的路径
  • scale:图像减均值后变换的尺度
  • image_root_dir:训练数据的根目录
  • source:训练数据的list路径
  • crop_size:图像crop的大小
  • ratio:正样本所占的比例(0~1)

caffe在train.prototxt中定义网络结构的时候,可以传入这些参数。我们目前只需要知道,这些参数一定可以获取到,就可以了。另外,source表示训练数据的list的文件地址,这里用到的训练数据的格式和Caffe打batch的数据一样。

file_path1    label1

file_path2    label2

这样的格式。

Data层的具体实现,首先需要继承caffe.Layer这个类,之后实现setup, forward, backward和reshape,不过data层并不需要backward和reshape。setup主要是为了初始化各种参数,并且设置top的大小。对于Data层来说,forward则是生成数据和label。

闲话少说,代码来见。

上述的代码可以根据给定的list,batch size,ratio等参数生成符合要求的data和label。这里还有一些问题需要注意:

  1. 对输入的参数没有检验。
  2. 没有对读取图像等操作做异常处理。因此如果很不幸地读到的图片路径不合法,那么程序突然死掉都是有可能的。。。小喵的数据都是可以读的,所以木有问题。
  3. 在选取正负样本对的时候,对于正样本对,只有样本对应的label中的图片数大于5的时候,才选正样本(小喵的训练数据每个人都有至少几十张图片,所以木有出现问题),如果样本比较少的话,可以更改这个数(特别是有测试集的时候,测试集通常数目都很少,小喵训练的时候都是不用测试集的,因为会死循环。。。)。对于选取负样本对的时候,只是随便选了两张图片,而并没有真的保证这一对是不同label,这里考虑到训练数据是比较多的,所以不大可能选中同一个label的样本,因此可以近似代替负样本对。
  4. 这里有个减均值的操作,这个均值文件是经过特殊转换求出的numpy的数组。Caffe生成的均值文件是不能直接用的,但是可以通过仿照Caffe中Classifier中的写法来代替(caffe.io.Transformer工具)。另外这里的图片数据和均值文件是一样大小的,但实际上可能并不一定相等。如果需要对输入图片做各种随机化的操作,还需要自己修改代码。

至此,我们就完成了一个简单的Data层了。

那么在么调用自己的data层呢?

这里有一个十分简单的写法。在我们用来训练的prototxt中,将Data层的定义改成如下的方式:

python_param中的这三个参数需要注意:

module:模块名,我们先前编写的data层,本身就是一个文件,也就是一个模块,因此模块名就是文件名。

layer:层的名字,我们在文件中定义的类的名字。这里比较巧合,module和layer的名字相同。

param_str:所有的需要传给data层的参数都通过这个参数来传递。这里简单的使用了Python字典的格式的字符串,在data层中使用eval来执行(o(╯□╰)o  这其实并不是一个好习惯),从而获取参数,当然也可以使用别的方式来传递,比如json或者xml等。

最后,你在训练的时候可能会报错,说找不到你刚刚的层,或者找不到caffe,只需要把这个层的代码所在的文件夹的路径加入到PYTHONPATH中即可。

这样就完成了我们的Data层的编写,是不是非常简单?

重要更新:
1,小喵最近发现直接在image_data_layer.cpp中进行修改,可以更好的实现这个目标,而且支持多GPU。
2,训练的数据可以只用正样本对,因为identity signal已经十分强调不同identity的feature之间的距离,因此verification signal只需要强调相同的identity的feature相近就好。
3,小喵新的训练数据,构造pair的方式也做了修改。每次使用所有的数据构造pair,然后用来训练,每个epoch后都重新生成一次list。这样可以保证identity signal能够每次训练所有的图片,而verification signal也能每次训练不同的样本对。

 

如果您觉得本文对您有帮助,那请小喵喝杯茶吧~~O(∩_∩)O~~

%e6%89%93%e8%b5%8f

 

转载请注明出处~

miao

miao

18 Comments

  1. 博主,你的cv2.read之后在加上.astype是做什么用的?我本地试了一张人脸图片,发现如果加上这个,在cvshow的时候,图像就剩边缘了。所以这个步骤做的目的是?

     

    • 图片就是矩阵,opencv图片本身像素值是uint8格存的,也就是unsigned char,做减法的时候可能会出现错误(溢出),所以这里转成float32。而且caffe的数据都是float的。

  2. 博主你好, 想请教一下, 每次随机取出的batch中positive跟negative pair的ratio是否一定要固定才训练的好呢? 谢谢

    • 不是,我有的同学只有正样本对也能训,说是identity的监督已经让不同人之间的距离变大了。另外normalization层也不是必须的,直接算id2 loss,然后把loss weight设很小也行。

  3. 请问博主,要是不用随机构建训练对的方法,用你说的最简单的构建有序训练对的方法,要怎么实现?需要更改数据层么?还有就是您最后说的重要更新,代码有开源吗?

    • 我用的策略是这样的,假定你有N个identity的总量M张图的数据集。
      先在每个identity中,随机构造正样本对。
      这样就可以得到约M/2个pair(考虑到每个identity的图不是偶数个),然后再讲这M/2个pair打乱顺序。用来训练。
      每次网络学习完这M张数据之后,再用相同的方法生成数据。
      这样的好处是,每次的batch的pair都是不同的,但是每个epoch中都训练了整个数据集。
      修改image_data_layer层可以很容易的实现。
      没有附上源码一是因为策略太简单,二是因为我这里的数据有一些其他的处理。有一些奇怪的参数。

  4. 博主,你好!
    有个问题想请教一下你,在deepid的网络结构中,第四个卷积层并没有进行权值的共享,这个在caffe中该如何进行配置呢?

    • 这个好像没什么办法了,直接读文件的话,硬盘的IO很影响训练速度。我训练的时候,用了SSD,所以速度比较快。

  5. 你好小喵咪~~~
    我按照你的方法进行了编写,只不过为了不编写之后的c++层,我将输出top变成了4个,分别是data,data_p, label, label_p。我把.py文件放在了caffe/python,也放在了 caffe/examples/DeepID2/,也在bashrc中改成export PYTHONPATH=PYTHONPATH:/home/caffe/examples/DeepID2/:/home/caffe/python
    但是不管哪种方法,都会报出下面的错误~~~
    你知道这是怎么回事吗~~~
    喵~~谢谢~~
    I0318 19:56:25.580164 58074 layer_factory.hpp:77] Creating layer pair_data
    *** Aborted at 1489838185 (unix time) try “date -d @1489838185” if you are using GNU date ***
    PC: @ 0x7f9061266faf dgetrf_

    *** SIGSEGV (@0x280) received by PID 58074 (TID 0x7f90ded95ac0) from PID 640; stack trace: ***
    @ 0x7f90dc4504b0 (unknown)
    @ 0x7f9061266faf dgetrf_
    @ 0x7f905d0e4f15 f2py_rout__flapack_dgetrf
    @ 0x7f90dcb0b1e3 PyObject_Call
    @ 0x7f90dcab613c PyEval_EvalFrameEx
    @ 0x7f90dcbe201c PyEval_EvalCodeEx
    @ 0x7f90dcab7cfd PyEval_EvalFrameEx
    @ 0x7f90dcbe201c PyEval_EvalCodeEx
    @ 0x7f90dcab0b89 PyEval_EvalCode
    @ 0x7f90dcb451b4 PyImport_ExecCodeModuleEx
    @ 0x7f90dcb45b8f (unknown)
    @ 0x7f90dcb47300 (unknown)
    @ 0x7f90dcb4855a PyImport_ImportModuleLevel
    @ 0x7f90dcabf698 (unknown)
    @ 0x7f90dcb0b1e3 PyObject_Call
    @ 0x7f90dcbe1447 PyEval_CallObjectWithKeywords
    @ 0x7f90dcab45c6 PyEval_EvalFrameEx
    @ 0x7f90dcbe201c PyEval_EvalCodeEx
    @ 0x7f90dcab0b89 PyEval_EvalCode
    @ 0x7f90dcb451b4 PyImport_ExecCodeModuleEx
    @ 0x7f90dcb45b8f (unknown)
    @ 0x7f90dcb46b32 (unknown)
    @ 0x7f90dcb47300 (unknown)
    @ 0x7f90dcb4855a PyImport_ImportModuleLevel
    @ 0x7f90dcabf698 (unknown)
    @ 0x7f90dcb0b1e3 PyObject_Call
    @ 0x7f90dcbe1447 PyEval_CallObjectWithKeywords
    @ 0x7f90dcab45c6 PyEval_EvalFrameEx
    @ 0x7f90dcbe201c PyEval_EvalCodeEx
    @ 0x7f90dcab0b89 PyEval_EvalCode
    @ 0x7f90dcb451b4 PyImport_ExecCodeModuleEx
    @ 0x7f90dcb45b8f (unknown)
    Segmentation fault (core dumped)

    • 你好,我没有碰到过类似的问题,而且上次写python层已经是很久之前了。这里只能给你两个建议:
      1,PYTHONPATH=PYTHONPATH:/home/caffe/examples/DeepID2/:/home/caffe/python
      如果你是这样导出的,那么可能就有问题。应该这么写:export PYTHONPATH=$PYTHONPATH:/home/caffe/examples/DeepID2/:/home/caffe/python
      2,如果不是上述的问题,你不妨先写一个简单的data层,看能不能工作,再改成你需要的复杂的层。
      希望能够帮到你。

  6. 请问下博主,

    因此verification signal只需要强调相同的identity的feature相近就好

    这个有什么依据吗,能贴一下出处吗?感谢

    • 你好啊~
      我印象中(很早之前了),deepid2论文说明只用正样本对、只用负样本对和一起使用的效果,只用正样本对效果和全部都用差不多。
      我自己这边的实验,最开始是用了负样本对的,有几个问题,margin不好估计,最终训练的结果也一般。不如直接全是正样本训练的方便,而且效果也很好。
      实验结果是找不到了,你想实验的话,改一下data层应该就很好实验。

      • 我试了一下,影响不是特别大,但是对结果会有一点点影响,而且速度也变慢了,没有同时用similar和dissimilar训练的快。(不过也有可能因为我做的不是deepid2)

        • 这样啊。确实是一个比较有意思的现象。😊
          我自己通常是先训练DeepID然后finetune DeepID2的,所以没有观察过速度等的问题。囧。

          可以问下,你主要是在做什么方面的研究吗?

发表评论

电子邮件地址不会被公开。