Firefly开源社区

基于MobileNet-SSD的目标检测Demo(一)

发表于 2018-9-28 10:17:16     
本帖最后由 暴走的阿Sai 于 2018-9-28 10:43 编辑

上一篇文章《训练MobileNet-SSD | Hey~YaHei!》介绍了如何训练自己的MobileNet-SSD模型并部署在Tengine平台上。
本文将继续尝试根据实际情况删减多余类别进行训练,并用Depthwise Convolution进一步替换Standard Convolution。


削减类别
VOC数据集包含二十个类别的物体,分别是——aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, foa, train, tvmonitor,有时候我们想用VOC数据集训练,但并不需要这么多类别,而caffe-ssd提供的数据处理工具create_list.sh和create_data.sh默认是处理所有的20个分类的。如果我们不想重写这些数据处理工具,可以从根源入手,也就是直接修改数据集里的标注信息,把多余分类的信息删去。

处理数据集
首先观察一下VOC数据集的结构——

  • Annotations:存放图片的标注信息,每张图片对应一个xml文件
  • ImageSets:存放图片的分类列表,包含三个子目录:
    • Layout:存放与人体部位有关的图片列表文件
    • Main:存放物体分类中每一个分类的图片列表文件
    • Segmentation:存放与图像分别有关的图片列表文件
  • JPEGImages:存放所有的图片
  • NewAnnotations:忽略吧……是我自己生成的目录
  • SegmentationClass:存放类别分割任务的蒙版文件
  • SegmentationObject:存放实体分割任务的蒙版文件

JPEGImages目录下每张图片都包含一到多个物体,这些物体的位置、类别信息都记录再Annotations目录下的同名xml文件中,文件内容类似:
  1. <annotation>
  2.         <folder>VOC2007</folder>
  3.         <filename>008973.jpg</filename>
  4.         <source>
  5.                 <database>The VOC2007 Database</database>
  6.                 <annotation>PASCAL VOC2007</annotation>
  7.                 <image>flickr</image>
  8.                 <flickrid>335707085</flickrid>
  9.         </source>
  10.         <owner>
  11.                 <flickrid>kjmurray</flickrid>
  12.                 <name>Katherine Murray</name>
  13.         </owner>
  14.         <size>
  15.                 <width>500</width>
  16.                 <height>333</height>
  17.                 <depth>3</depth>
  18.         </size>
  19.         <segmented>1</segmented>
  20.         <object>
  21.                 <name>cow</name>
  22.                 <pose>Unspecified</pose>
  23.                 <truncated>0</truncated>
  24.                 <difficult>0</difficult>
  25.                 <bndbox>
  26.                         <xmin>271</xmin>
  27.                         <ymin>43</ymin>
  28.                         <xmax>444</xmax>
  29.                         <ymax>279</ymax>
  30.                 </bndbox>
  31.         </object>
  32. </annotation>
复制代码

而caffe-ssd的数据处理工具正是根据这些xml文件提供的标记进行处理的,所以说,我们可以通过遍历xml文件,判断object的类别,如果是我们不需要的,则把对应的object标签删去来达到削减类别的目的,除此之外还要处理对应的图片路径列表和图片大小列表,删除多余的项。
根据这一思路,可以写一个简单的py脚本来实现:
  1. #!/usr/bin/python
  2. #-*- coidng: utf-8 -*-

  3. import xml.etree.ElementTree as ET
  4. import os

  5. VOC_ROOT = "/home/zhengkai/data/VOCdevkit/"
  6. CLASS2KEEP = ('background',
  7.             'aeroplane', 'bird',
  8.             'bottle', 'bus', 'car', 'chair',
  9.             'dog', 'bicycle', 'motorbike',
  10.             'boat', 'sofa')

  11. def process_xml(src_path, dst_path):
  12.     """
  13.     解析并处理xml文件——
  14.     解析源文件,删除无关object标签,
  15.     如果存在有效object,则写入到目标文件,并返回True;
  16.     否则,直接返回False。
  17.     """

  18.     tree = ET.parse(src_path)
  19.     root = tree.getroot()
  20.     no_objs = True
  21.     for obj in root.findall("object"):
  22.         cls = obj.find("name").text
  23.         if cls not in CLASS2KEEP:
  24.             root.remove(obj)
  25.         else:
  26.             no_objs = False
  27.     if not no_objs:
  28.         tree.write(dst_path)
  29.         return True
  30.     else:
  31.         return False

  32. if __name__ == "__main__":
  33.     # 记录有效的xml文件名
  34.     valid_lst = []

  35.     # 处理xml文件,新的xml文件写入到NewAnnotations目录下
  36.     for dataset in ["VOC2007/", "VOC2012/"]:
  37.         raw_anno_dir = VOC_ROOT + dataset + "Annotations/"
  38.         dst_anno_dir = VOC_ROOT + dataset + "NewAnnotations/"

  39.         if not os.path.exists(dst_anno_dir):
  40.             print("Create a new dir: " + dst_anno_dir)
  41.             os.mkdir(dst_anno_dir)

  42.         for xml_filename in os.listdir(raw_anno_dir):
  43.             if process_xml(raw_anno_dir + xml_filename, dst_anno_dir + xml_filename):
  44.                 valid_lst.append(xml_filename.split(".")[0])

  45.     # 处理图片路径列表txt文件,根据valid_lst筛选有效的图片路径
  46.     for filename in ["test.txt", "trainval.txt"]:
  47.         with open("new_" + filename, "w") as nf:
  48.             with open(filename, "r") as of:
  49.                 for line in of.readlines():
  50.                     if line.split("/")[-1].split(".")[0] in valid_lst:
  51.                         nf.write(line.replace("Annotations", "NewAnnotations"))

  52.     # 处理图片大小列表txt文件,根据valid_lst筛选有效的图片大小
  53.     with open("new_test_name_size.txt", "w") as nf:
  54.         with open("test_name_size.txt", "r") as of:
  55.             for line in of.readlines():
  56.                 if line.split(" ")[0] in valid_lst:
  57.                     nf.write(line.replace("Annotations", "NewAnnotations"))
复制代码

生成lmdb文件
修改caffe-ssd数据处理工具中的标签映射文件labelmap_voc.prototxt,该文件由若干个类似下边的item组成:
  1. item {
  2.     name: "none_of_the_above"
  3.     label: 0
  4.     display_name: "background"
  5. }
复制代码

  • name:物体类别在xml文件中出现的名称
  • label:标签对应的数值(为方便处理,建议序号从0递增)
  • display_name:该类别最后要展示出来的名称

删除映射文件中多余类别对应的item,然后按顺序重新为各个类别编号(修改label项);
修改create_list.sh脚本,将第29行的Annotations改为NewAnnotations——
  1. [27] label_file=$bash_dir/$dataset"_label.txt"
  2. [28] cp $dataset_file $label_file
  3. [29] sed -i "s/^/$name\/NewAnnotations\//g" $label_file
  4. [30] sed -i "s/$/.xml/g" $label_file
复制代码

然后跟上一篇文章一样,依次执行脚本create_list.sh和create_data.sh即可。

训练和部署
训练和部署过程与 《训练MobileNet-SSD/开始训练MobileNet-SSD/训练(部署) | Hey~YaHei!》 基本相同;
微小的区别在于,
  • 生成模型文件时
    ./gen_model.sh 21中21要换成实际的类别数量(含背景background);
  • 要使用新的标签映射文件labelmap.prototxt;
  • 应用程序中标签要对应修改
    如《RK3399上Tengine平台搭建/目标检测网络MobileNet-SSD | Hey~YaHei!》最后列出的代码中,post_process_ssd函数里的class_names数组常量要对应修改(索引号与labelmap.prototxt文件里的label标签一一对应)。

Depthwise Convolution和Standard Convolution(Group)的比较
观察chuanqi305的 MobileNet-SSD模型文件deploy.prototxt 可以发现,其中的Depthwise Convolution都是使用特殊的caffe原生卷积层(group参数与num_output参数相等)来实现的。

查阅caffe官方文档,
  1. group (g) [default 1]: If g > 1, we restrict the connectivity of each filter to a subset of the input. Specifically, the input and output channels are separated into g groups, and the ith output group channels will be only connected to the ith input group channels.
复制代码

可以知道,group参数强制输入通道和输出通道分为若干组,一组输入通道卷积运算得到组序相同的输出通道,所以使 group == num_output 确实可以得到与Depthwise Convolution相同的结果。但是,“分组”?从字面的意思上看,不免让人怀疑,这个group是不是通过简单的循环实现的,如果真是如此,不同的group还会并行的计算吗?我们不妨做个简单的实验——

我在github上找到第三方实现的 caffe - Depthwise Convolution层,根据其README的说明将其放到caffe源码下重新编译caffe-ssd得到专门实现的DepthwiseConvolution。
在单卡GTX1080Ti、Intel E5-2683环境下测试结果如下(分别重复运行100次,单位:秒/百次):

caffe-mode
Standard Convolution(Group)
Depthwise Convolution
cpu-only
26.13568
23.40233
gpu
6.93938
0.53499
gpu-cudnn
6.86799
0.53779

很显然,在cpu-only模式下,两者没有太大区别;在gpu模式下(无论是caffe自身的加速库还是cudnn加速库),专门实现的DepthwiseConvolution都要快10倍左右!
除此之外,Depthwise Convolution Layer | github的README也给出了一些测试数据。

那Tengine有专门实现的DepthwiseConvolution层吗?
官方的文档上看,确实没有专门的DepthwiseConvolution层,如果试着在模型文件里使用DepthwiseConvolution也会看到报错。不过!在源码 executor/operator/arm64/conv/conv_2d_dw.cpp | github, Tengine可以看到有一个 isDepthwiseSupported 函数——
  1. static bool isDepthwiseSupported(const ConvParam * param, const TShape& input_shape)
  2. {
  3.     int input_c=input_shape.GetC();
  4.     int group=param->group;
  5.     int kernel_h=param->kernel_h;
  6.     int kernel_w=param->kernel_w;
  7.     int stride_h=param->stride_h;
  8.     int stride_w=param->stride_w;
  9.     int dilation_h=param->dilation_h;
  10.     int dilation_w=param->dilation_w;
  11.     int pad_h0=param->pads[0];
  12.     int pad_w0=param->pads[1];
  13.     int pad_h1=param->pads[2];
  14.     int pad_w1=param->pads[3];

  15.     if(group == 1 || input_c != group || kernel_h != 3 || kernel_w != 3 ||
  16.        pad_h0 != 1 || pad_w0 !=1 || pad_h0 != pad_h1 || pad_w0 != pad_w1 ||
  17.        dilation_h != 1 || dilation_w != 1 || stride_w != stride_h)
  18.     {
  19.         return false;
  20.     }
  21.     return true;
  22. }
复制代码

也就是说,Tengine会自行判断Convolution层是否属于DepthwiseConvolution并相应作出优化,对应的汇编实现为 executor/operator/arm64/conv/dw_k3s1p1.S | github, Tengine

进一步替换Depthwise Convolution
从《MobileNet-SSD网络解析 | Hey~YaHei!》一文中可以看到,chuanqi305在设计MobileNet-SSD时还是保守地在Conv14_1到Conv17_2使用Standard Convolution,我们不妨进一步把这一部分也替换为深度向分解的卷积,替换的方式也很简单,举个例子:
对于某个传统的Convolution层
  1. layer {
  2.   name: "conv14_2"
  3.   type: "Convolution"
  4.   bottom: "conv14_1"
  5.   top: "conv14_2"
  6.   param {
  7.     lr_mult: 1.0
  8.     decay_mult: 1.0
  9.   }
  10.   param {
  11.     lr_mult: 2.0
  12.     decay_mult: 0.0
  13.   }
  14.   convolution_param {
  15.     num_output: 512
  16.     pad: 1
  17.     kernel_size: 3
  18.     stride: 2
  19.     weight_filler {
  20.       type: "msra"
  21.     }
  22.     bias_filler {
  23.       type: "constant"
  24.       value: 0.0
  25.     }
  26.   }
  27. }
  28. layer {
  29.   name: "conv14_2/relu"
  30.   type: "ReLU"
  31.   bottom: "conv14_2"
  32.   top: "conv14_2"
  33. }
复制代码

修改为
  1. layer {
  2.   name: "conv14_2_new/dw"
  3.   type: "DepthwiseConvolution"
  4.   bottom: "conv14_1_new"
  5.   top: "conv14_2_new/dw"
  6.   param {
  7.     lr_mult: 0.1
  8.     decay_mult: 0.1
  9.   }
  10.   convolution_param {
  11.     num_output: 256
  12.     bias_term: false
  13.     pad: 1
  14.     kernel_size: 3
  15.     stride: 2
  16.     group: 256
  17.     engine: CAFFE
  18.     weight_filler {
  19.       type: "msra"
  20.     }
  21.   }
  22. }
  23. layer {
  24.   name: "conv14_2_new/dw/relu"
  25.   type: "ReLU"
  26.   bottom: "conv14_2_new/dw"
  27.   top: "conv14_2_new/dw"
  28. }
  29. layer {
  30.   name: "conv14_2_new"
  31.   type: "Convolution"
  32.   bottom: "conv14_2_new/dw"
  33.   top: "conv14_2_new"
  34.   param {
  35.     lr_mult: 0.1
  36.     decay_mult: 0.1
  37.   }
  38.   convolution_param {
  39.     num_output: 512
  40.     bias_term: false
  41.     kernel_size: 1
  42.     weight_filler {
  43.       type: "msra"
  44.     }
  45.   }
  46. }
  47. layer {
  48.   name: "conv14_2_new/relu"
  49.   type: "ReLU"
  50.   bottom: "conv14_2_new"
  51.   top: "conv14_2_new"
  52. }
复制代码

要注意几点,

  • 修改后的层要给个新的名字,避免初始化权重的时候从预训练好的模型误导入权重;
  • 训练模型train.prototxt和测试模型test.prototxt可别忘了加上BN层;
  • 部署在Tengine上的时候要记得把type从DepthwiseConvolution替换为Convolution。
……其他层也做类似的修改即可。

替换前后比较——

VOC2007-test
MobileNet-SSD
MobileNet-SSDLite
mAP
0.727
0.718
FPS(1080Ti)
258
278
caffemodel
23MB
16MB





本文介绍了如何削减VOC数据集上多余类别进行训练,并且尝试用深度向分解的卷积层进一步替换传统的卷积层,同时比较了专门优化加速的DepthwiseConvolution和Convolution(Group)在效率上的差别。
下一篇文章《基于MobileNet-SSD的目标检测Demo(二)》将介绍如何把目标检测和视频解码与显示分别放到两个线程上,来提高目标检测demo的流畅性。















暴走的创客!
回复

使用道具 举报

本版积分规则

友情链接 : 爱板网 电子发烧友论坛 云汉电子社区 粤ICP备14022046号-2
快速回复 返回顶部 返回列表