[深度学习] RCNNs系列(1) Ubuntu下Faster RCNN配置及训练和测试自己的数据方法

最近用到Faster RCNN进行目标检测,前前后后两周把RCNN,SPPNet,Fast RCNN和Faster RCNN大体调查了一遍,准备写一个RCNNs系列,后面还要加上今年最新的Mask RCNN。

要想开个头,知道RCNNs在目标检测方向的优势,那就先用用作者的代码,跑跑自己的代码,下面就是在Ubuntu下进行Faster RCNN配置的方法。

一、Faster RCNN环境配置及demo运行

虽然Faster RCNN中作者加入了很多新的东西,比如怎么选Anchar,怎么计算多任务的loss等等,所幸的是作者开源了代码,让我们很容易就能够用他的算法实现我们自己的任务。在运行自己的任务之前,我们首先要做的就是确保我们已经配置好源代码的运行环境。
本文是在Caffe已经配置好的条件下进行的,如果Caffe的需求库,比如opencv等等还没有弄好,建议先去把Caffe配置好。
(1)首先使用git把Faster RCNN的源码下载到本地:
  1. git clone --recursive https://github.com/rbgirshick/py-faster-rcnn.git    
(2)安装cpython和python-opencv
  1. pip install cpython  
  2. apt-get install python-opencv  
(3)下载Faster RCNN并安装好cpython以后,进入py-faster-rcnn/lib中使用命令编译一下
  1. make  
(4)进入py-faster-rcnn/caffe-fast-rcnn,配置Makefile.config文件,目录中有Makefile.config.example文件,可以按照这个文件进行修改,或者使用你配置caffe时的Makefile.config文件。我这里有一个配置文件,可以参考。我的配置文件用到了cuDNN,开始我使用cuDNN的时候总会在编译时报错,后来在在windows上配置py-faster-rcnn和调参经验中找到了解决办法,解决办法如下:
  • 把Caffe中的include/caffe/layers/cudnn_relu_layer.hpp,/include/caffe/layers/cudnn_sigmoid_layer.hpp,/include/caffe/layers/cudnn_tanh_layer.hpp和/include/caffe/util/cudnn.hpp替换py-faster-rcnn/caffe-fast-rcnn中对应的文件;
  • 把Caffe中的src/caffe/layers/cudnn_relu_layer.cpp,src/caffe/layers/cudnn_relu_layer.cu,src/caffe/layers/cudnn_sigmoid_layer.cpp,src/caffe/layers/cudnn_sigmoid_layer.cu,src/caffe/layer/cudnn_tanh_layer.cpp,src/caffe/layers/cudnn_tanh_layer.cu替换掉py-faster-rcnn/caffe-fast-rcnn中对应的文件;
  • 把py-faster-rcnn/caffe-fast-rcnn中src/caffe/layers/cudnn_conv_layer.cu文件中的cudnnConvolutionBackwardData_v3全部换为cudnnConvolutionBackwardData,把cudnnConvolutionBackwardFilter_v3全部换为cudnnConvolutionBackwardFilter。
如果自己的显卡配置足够的话,强烈建议开启cudnn,cudnn不仅可以起到加速的作用,而且我跑实验的时候发现加入cudnn以后可以明显的降低显存的使用。如下图所示:
开启cudnn之前:
[深度学习] RCNNs系列(1) Ubuntu下Faster RCNN配置及训练和测试自己的数据方法
开启cudnn之后:
[深度学习] RCNNs系列(1) Ubuntu下Faster RCNN配置及训练和测试自己的数据方法
可以看到显存减少了将近2G,非常可观,强烈建议开启。
(5)配置好Makefile以后,在caffe-fast-rcnn中执行命令进行编译
  1. make -j8 && make pycaffe  
(6)这个时候,基本上faster RCNN也就配置好了,让我们看一下是否真的能够运行。首先我们下载几个作者已经训练好的caffemodel,如果现在直接运行demo.py的话会提示你没有caffemodel文件,然后询问是否运行过py-faster-rcnn/data/scripts/fetch_faster_rcnn_models.sh文件,我们可以找到该文件中的下载链接,用迅雷一会就可以下载好,如果直接运行该文件可能要不少时间。把下载好的文件中VGG16_faster_rcnn_final.caffemodel文件复制到py-faster-rcnn/data/faster_rcnn_models中
(7)运行py-faster-rcnn/experiments/tools/demo.py文件,不出意外的话,会出现如下错误:
[深度学习] RCNNs系列(1) Ubuntu下Faster RCNN配置及训练和测试自己的数据方法
修改py-faster-rcnn/lib/fast_rcnn/train.py文件,在文件中引入text_format
  1. import google.protobuf.text_format  
再次运行demo.py文件,这次应该可以运行成功!

二、Faster RCNN训练自己的数据

既然已经把Faster RCNN配置好,下一步我们来训练自己的数据吧。Faster RCNN论文中采用的训练方法分为几个阶段,训练起来比较麻烦,我们这里采用源码中的end to end的训练方式,更简便一些。

2.1 建立数据集

为了让我们的训练更简单些,我们不去改动源码中读写数据的方式,而是把我们的数据集改成Pascal VOC的数据集格式,Pascal VOC数据集主要分为三个部分:Annotations,JPEGImages和ImageSets。其中JPEGImages中存放的是训练和测试时需要的图像;Annotations存放的是每个图像中所有目标的bounding box信息,每个图像对应一个xml文件;ImageSet文件中存放的Main目录,而Main目录中就是训练和测试时需要的文件列表,主要分为train.txt, test.txt, trainval.txt, val.txt可以根据文件名就知道哪些是训练数据列表,哪些是测试数据列表。
这里如果我们有这样结构的数据集当然最好了,如果没有这样的数据集的话就需要自己建立。建立数据集的话可以参考:将数据集做成VOC2007格式用于Faster-RCNN训练——小咸鱼_的博客
这里解说一下我的获取方法,因为我的数据比较特殊,是由在线数据转换过来的,所以我很容易的就获得了各个目标的bounding box信息,所以没有使用参考博客的方法。我最终获得的bounding box信息文件如下所示:
  1. Train_IMG\000001.jpg 97 6 174 202 305  
  2. Train_IMG\000001.jpg 8 56 198 282 162  
  3. ......省略若干行  
  4. Train_IMG\000001.jpg 90 537 194 699 314  
其中每个图像对应一个bounding box文件,每个文件中的每一行表示一个目标的bounding box,每一行由6列数据组成,第1列数据为图像的路径,第2列为该目标的分类,第3列为bounding box的左上角的x(对应图像的列,即mincol),第4列为bounding box的左上角的y(对应图像的行,即minrow),第5列为bounding box的右下角的x(对应图像的列,即maxcol),第6列为bounding box右下角的y(对应图像的行,即maxrow)。
然后我们接下来要做的就是把这些bounding box文件转换为VOC数据集格式的xml文件,我这里是从github上找到的一个Python开源代码(但是作者的原址找不到了,所以这里没能给出参考链接,如果有人有原址欢迎告知,我会把原址贴上)上进行的改动,源码如下:
  1. # -*- coding:utf-8 -*-  
  2.   
  3. __author__ = "peic"  
  4.   
  5. import xml.dom  
  6. import xml.dom.minidom  
  7. import os  
  8.   
  9. from PIL import Image  
  10.   
  11. ''''' 
  12. 根据下面的路径和文件,将output.txt制作成xml的标注 
  13. '''  
  14.   
  15. # xml文件规范定义  
  16. _INDENT = ' ' * 4  
  17. _NEW_LINE = '\n'  
  18. _FOLDER_NODE = 'VOC2007'  
  19. _ROOT_NODE = 'annotation'  
  20. _DATABASE_NAME = 'CROHME offline ME Dataset'  
  21. _CLASS = 'person'  
  22. _ANNOTATION = 'PASCAL VOC2007'  
  23. _AUTHOR = 'hanchao'  
  24.   
  25. _SEGMENTED = '0'  
  26. _DIFFICULT = '0'  
  27. _TRUNCATED = '0'  
  28. _POSE = 'Unspecified'  
  29. #需要注意,这一项是存放bounding box对应图像的目录  
  30. _IMAGE_PATH = 'Train_IMG'  
  31. #这一项是保存生成xml文件的目录  
  32. _ANNOTATION_SAVE_PATH = 'Annotations'  
  33.   
  34. _IMAGE_CHANNEL = 3  
  35.   
  36.   
  37. # 封装创建节点的过程  
  38. def createElementNode(doc, tag, attr):  
  39.     # 创建一个元素节点  
  40.     element_node = doc.createElement(tag)  
  41.   
  42.     # 创建一个文本节点  
  43.     text_node = doc.createTextNode(attr)  
  44.   
  45.     # 将文本节点作为元素节点的子节点  
  46.     element_node.appendChild(text_node)  
  47.       
  48.     return element_node  
  49.   
  50.   
  51. # 封装添加一个子节点的过程  
  52. def createChildNode(doc, tag, attr, parent_node):  
  53.   
  54.     child_node = createElementNode(doc, tag, attr)  
  55.     parent_node.appendChild(child_node)  
  56.   
  57. # object节点比较特殊  
  58. def createObjectNode(doc, attrs):  
  59.     object_node = doc.createElement('object')  
  60.     createChildNode(doc, 'name', attrs['classification'], object_node)  
  61.     createChildNode(doc, 'pose', _POSE, object_node)  
  62.     createChildNode(doc, 'truncated', _TRUNCATED, object_node)  
  63.     createChildNode(doc, 'difficult', _DIFFICULT, object_node)  
  64.   
  65.     bndbox_node = doc.createElement('bndbox')  
  66.     createChildNode(doc, 'xmin', attrs['xmin'], bndbox_node)  
  67.     createChildNode(doc, 'ymin', attrs['ymin'], bndbox_node)  
  68.     createChildNode(doc, 'xmax', attrs['xmax'], bndbox_node)  
  69.     createChildNode(doc, 'ymax', attrs['ymax'], bndbox_node)  
  70.     object_node.appendChild(bndbox_node)  
  71.   
  72.     return object_node  
  73.   
  74. # 将documentElement写入XML文件中  
  75. def writeXMLFile(doc, filename):  
  76.     tmpfile = open('tmp.xml''w')  
  77.     doc.writexml(tmpfile, addindent=' '*4, newl='\n', encoding='utf-8')  
  78.     tmpfile.close()  
  79.   
  80.   
  81.     # 删除第一行默认添加的标记  
  82.     fin = open('tmp.xml')  
  83.     fout = open(filename, 'w')  
  84.     lines = fin.readlines()  
  85.   
  86.     for line in lines[1:]:  
  87.         if line.split():  
  88.             fout.writelines(line)  
  89.   
  90.     #new_lines = ''.join(lines[1:])  
  91.     #fout.write(new_lines)  
  92.     fin.close()  
  93.     fout.close()  
  94.   
  95. # 创建XML文档并写入节点信息  
  96. def createXMLFile(attrs, width, height, filename):  
  97.   
  98.     # 创建文档对象, 文档对象用于创建各种节点  
  99.     my_dom = xml.dom.getDOMImplementation()  
  100.     doc = my_dom.createDocument(None, _ROOT_NODE, None)  
  101.   
  102.     # 获得根节点  
  103.     root_node = doc.documentElement  
  104.   
  105.     # folder节点  
  106.     createChildNode(doc, 'folder', _FOLDER_NODE, root_node)  
  107.       
  108.     # filename节点  
  109.     createChildNode(doc, 'filename', attrs['name'], root_node)  
  110.   
  111.     # source节点  
  112.     source_node = doc.createElement('source')  
  113.     # source的子节点  
  114.     createChildNode(doc, 'database', _DATABASE_NAME, source_node)  
  115.     createChildNode(doc, 'annotation', _ANNOTATION, source_node)  
  116.     createChildNode(doc, 'image''flickr', source_node)  
  117.     createChildNode(doc, 'flickrid''NULL', source_node)  
  118.     root_node.appendChild(source_node)  
  119.   
  120.     # owner节点  
  121.     owner_node = doc.createElement('owner')  
  122.     # owner的子节点  
  123.     createChildNode(doc, 'flickrid''NULL', owner_node)  
  124.     createChildNode(doc, 'name', _AUTHOR, owner_node)  
  125.     root_node.appendChild(owner_node)  
  126.   
  127.     # size节点  
  128.     size_node = doc.createElement('size')  
  129.     createChildNode(doc, 'width', str(width), size_node)  
  130.     createChildNode(doc, 'height', str(height), size_node)  
  131.     createChildNode(doc, 'depth', str(_IMAGE_CHANNEL), size_node)  
  132.     root_node.appendChild(size_node)  
  133.   
  134.     # segmented节点  
  135.     createChildNode(doc, 'segmented', _SEGMENTED, root_node)  
  136.   
  137.     # object节点  
  138.     object_node = createObjectNode(doc, attrs)  
  139.     root_node.appendChild(object_node)  
  140.   
  141.     # 写入文件  
  142.     writeXMLFile(doc, filename)  
  143.   
  144. def generate_xml(txt_filename):  
  145.     #注意,这里的txt_filename文件是待转换的bounding box文件  
  146.     ouput_file = open(txt_filename)  
  147.     current_dirpath = os.path.dirname(os.path.abspath('__file__'))  
  148.   
  149.     if not os.path.exists(_ANNOTATION_SAVE_PATH):  
  150.         os.mkdir(_ANNOTATION_SAVE_PATH)  
  151.   
  152.     lines = ouput_file.readlines()  
  153.     for line in lines:  
  154.         s = line.rstrip()  
  155.         array = s.split(' ')  
  156.         #print len(array)  
  157.         attrs = dict()  
  158.         attrs['name'] = array[0].split('\\')[1]  
  159.         attrs['classification'] = array[1]  
  160.         attrs['xmin'] = array[2]  
  161.         attrs['ymin'] = array[3]  
  162.         attrs['xmax'] = array[4]  
  163.         attrs['ymax'] = array[5]  
  164.   
  165.         # 构建XML文件名称  
  166.         xml_file_name = os.path.join(_ANNOTATION_SAVE_PATH, (attrs['name'].split('.'))[0] + '.xml')  
  167.         #print xml_file_name  
  168.   
  169.         if os.path.exists( xml_file_name):  
  170.             # print('do exists')  
  171.             existed_doc = xml.dom.minidom.parse(xml_file_name)  
  172.             root_node = existed_doc.documentElement  
  173.               
  174.             # 如果XML存在了, 添加object节点信息即可  
  175.             object_node = createObjectNode(existed_doc, attrs)  
  176.             root_node.appendChild(object_node)  
  177.   
  178.             # 写入文件  
  179.             writeXMLFile(existed_doc, xml_file_name)  
  180.               
  181.         else:  
  182.             # print('not exists')  
  183.             # 如果XML文件不存在, 创建文件并写入节点信息  
  184.             img_name = attrs['name']  
  185.             img_path = os.path.join(current_dirpath, _IMAGE_PATH, img_name)  
  186.             # 获取图片信息  
  187.             img = Image.open(img_path)  
  188.             width, height = img.size  
  189.             img.close()  
  190.               
  191.             # 创建XML文件  
  192.             createXMLFile(attrs, width, height, xml_file_name)  
  193.               
执行的Main程序如下:
  1. #coding=utf-8  
  2. ''''' 
  3. Created on 2017年4月18日 
  4.  
  5. @author: hanchao 
  6. '''  
  7. import generate_xml  
  8. import os.path  
  9. import cv2  
  10.   
  11. if __name__ == "__main__":  
  12.     for dir,path,filenames in os.walk('Train_BB'):#Train_BB是存放bounding box文件的目录  
  13.         for filename in filenames:  
  14.             print dir  + '/' + filename  
  15.             generate_xml.generate_xml(dir  + '/' + filename)  
最终生成的xml文件会保存在Annotations目录中。
对于ImageSets/Main中的几个文件就比较好建立了,只需要把训练、验证和测试集中图像名称写入到对应的文件中即可。
接下来,可以自己在py-faster-rcnn/data中建立一个VOCdevkit2007目录,在该目录中建立VOC2007子目录,然后把JPEGImages,Annotaions和ImageSets目录复制到该目录中,即准备好了自己的数据。
注意:Faster RCNN对图像和目标的大小以及长宽比是有一定要求的,具体的讨论见用ImageNet的数据集(ILSVRC2014)训练Faster R-CNN——Jiajun的博客
在这里我有深痛的教训T_T,我的数据集中设置目标的长宽比在0.1~10之间,图像的长宽比在0.3~7之间

2.2 训练自己的数据

经过前面的铺垫,我们终于可以训练自己的数据了。
接下来我们需要下载几个模型,看过论文的都知道,作者的训练首先使用在ImageNet上训练好的模型对网络结构进行初始化,然后再训练的网络,我们现在下载的模型就是进行初始化的模型,下载地址:https://pan.baidu.com/s/1c2tfkRm 下载完成后,把文件加压把模型放在py-faster-rcnn/data/imagenet_models中即可。
接下来,我们只需要更改几个文件即可。(注意,我这里使用的是端到端的训练方式,所以改动全部为end to end的,如果使用论文中的分阶段训练的方式,则需要改动alt_opt对应的文件)
(1)更改py-faster-rcnn/experiments/scripts/faster_rcnn_end2end.sh文件,该文件中的问题在于没有设置各个文件的绝对路径,所以运行起来可能有问题,把两个time后的路径设置加上py-faster-rcnn的绝对路径即可,我更改后示例如下:
  1. time /home/hanchao/py-faster-rcnn/tools/train_net.py --gpu ${GPU_ID} \  
  2.   --solver /home/hanchao/py-faster-rcnn/models/${PT_DIR}/${NET}/faster_rcnn_end2end/solver.prototxt \  
  3.   --weights /home/hanchao/py-faster-rcnn/data/imagenet_models/${NET}.v2.caffemodel \  
  4.   --imdb ${TRAIN_IMDB} \  
  5.   --iters ${ITERS} \  
  6.   --cfg /home/hanchao/py-faster-rcnn/experiments/cfgs/faster_rcnn_end2end.yml \  
  7.   ${EXTRA_ARGS}  
(2)更改py-faster-rcnn/models/pascal_voc/VGG16/faster_rcnn_end2end目录中的train,test和solver文件(我采用的是VGG16网络,如果使用ZF网络,去修改对应目录下文件即可)。把train和test中所有的21和84改成你的分类类型数+1(你的分类类型数+1)×4即可。在solver文件中加入绝对路径;
(3)更改py-faster-rcnn/lib/datasets/pascal_voc.py文件,更改self._classes中标签换成自己的标签,即
  1. self._classes = ('__background__'# always index 0  
  2.                  '你的标签1''你的标签2''你的标签3')  
注意这里的标签不要有大写符号。
(4)文章中还采用了horizontal flip以扩充数据, 我的数据是字符,因此不能扩充,所以我把py-faster-rcnn/lib/datasets/imdb.py中append_flipped_images(self)函数改为
  1.     def append_flipped_images(self):  
  2. #        num_images = self.num_images  
  3. #        widths = self._get_widths()  
  4. #        for i in xrange(num_images):  
  5. #            boxes = self.roidb[i]['boxes'].copy()  
  6. #            oldx1 = boxes[:, 0].copy()  
  7. #            oldx2 = boxes[:, 2].copy()  
  8. #            boxes[:, 0] = widths[i] - oldx2 - 1  
  9. #            boxes[:, 2] = widths[i] - oldx1 - 1  
  10. #            assert (boxes[:, 2] >= boxes[:, 0]).all()  
  11. #            entry = {'boxes' : boxes,  
  12. #                     'gt_overlaps' : self.roidb[i]['gt_overlaps'],  
  13. #                     'gt_classes' : self.roidb[i]['gt_classes'],  
  14. #                     'flipped' : True}  
  15. #            self.roidb.append(entry)  
  16. #        self._image_index = self._image_index * 2  
  17.         self._image_index = self._image_index  
如果你的数据比较多的话同样也可以去掉这个函数,该函数也可能会引发一些问题,具体更改和解决方法参见Faster-RCNN+ZF用自己的数据集训练模型(Python版本)——小咸鱼_的博客
好啦,接下来应该就可以愉快的训练自己的数据了,进入py-faster-rcnn/experiments/scripts目录,使用命令
  1. faster_rcnn_end2end.sh 0 VGG16 pascal_voc  
其中0表示你训练时使用的gpu标号,VGG16是模型类型,具体的内容可以去读faster_rcnn_end2end.sh的源码。
(注意:如果更改数据以后,再次训练以前一定要把py-faster-rcnn/data中的cache目录删掉,否则训练时用的还是以前的数据)
训练结束后,把训练好的模型放入到py-faster-rcnn/data/faster_rcnn_models中,然后调用把demo.py中的测试图片换成自己的测试图像,运行进行测试,如果有需要,也可以自己修改demo文件,程序还是很容易看懂的。

这就是训练的全部过程了,因为配置起来也有一定的时间了,有些错误也记不太清了,我把我还记得的坑都写在上面了,如果配置过程中有其他的一些错误,欢迎在评论里一起交流。

附录

————————————————————————
1. 在测试的时候,可能会出现IndexError: too many indices for array,出现这个错误的原因是你的测试集中有的类别没有出现,在这里只需要在py-faster-rcnn/lib/datasets/voc_eval.py中BB = BB[sorted_ind, :]前一句加上if len(BB) != 0:即可。
2. 在测试时还有可能出现KeyError: '某样本名',这是因为你数据更改了,但是程序有cache文件,把py-faster-rcnn/data/VOCdevkit2007/annotations_cache目录删除即可。