将 Faster R-CNN 用在自己的数据集上


发布于

|

分类

历经一个多月,也钻了很多牛角尖,终于成功一次,把过程 Log 贴出来备忘。

如果这篇文章对你有帮助,或者有任何什么问题,都欢迎在下面留言。

获取并修改代码

首先,我们需要获取源代码:

git clone --recursive https://github.com/rbgirshick/py-faster-rcnn.git

之后是对代码的各种修改:

Caffe 版本升级(可选)

大神是自己将 Caffe fork 了一份,然后进行魔改,之后就弃坑了。最新的 Caffe 支持 Cuda8.0 等各种加成,但是这里面的版本就不支持。 如果你喜欢折腾的话,可以将自带的 Caffe 进行一下升级:

cd caffe-fast-rcnn  
git remote add caffe https://github.com/BVLC/caffe.git  
git fetch caffe  
git merge -X caffe/master

这里请注意一下,在 2017 年 1 月 10 日之前,官方 Caffe 版本还是 RC3,直接 merge 就可以了;但是在 2017 年 2 月 5 日之后,直接 merge 会出错。哪位大神如果有了新的 Merge 方法,或者消除冲突的办法,可以在下面回复,感激不尽。

更新: 可以合并 RC5 这个 Tag,然后只保留远端版本。

错误修正

遇到冲突的文件,我只保留了远程更改。

找到 $FRCN_ROOT/caffe-fast-rcnn/include/caffe/layers/python_layer.hpp,将里面的 self_.attr("phase") = static_cast<int>(this->phase_); 注释掉。

找到 $FRCN_ROOT/lib/roi_data_layer/minibatch.py,将 173 行左右的 cls = clss[ind] 改为 cls = int(clss[ind])

消除一个编译错误

找到下面两个文件

$FRCN_ROOT/caffe-fast-rcnn/src/caffe/test/test_smooth_L1_loss_layer.cpp
$FRCN_ROOT/caffe-fast-rcnn/src/caffe/test/test_roi_pooling_layer.cpp

去掉最前面的

typedef ::testing::Types<GPUDevice<float>, GPUDevice<double> > TestDtypesGPU;

并将文件里面的 TestDtypesGPU 改为 TestDtypesAndDevices

另外,在 $FRCN_ROOT/caffe-fast-rcnn/src/caffe/test/test_smooth_L1_loss_layer.cpp 中,我们还需要去掉下面这行代码才能通过编译:

#include "caffe/vision_layers.hpp"

消除一个路径错误

$FRCN_ROOT/lib/setup.py 中需要使用 GPU 来编译一个叫做「nms」的东西(非极大值抑制)。

在这个文件的 locate_cuda 函数中,我们可能需要手动指定一下 cuda 的路径。

修正几个 Typo

  1. $FRCN_ROOT/lib/fast_rcnn/train.py 中添加 import google.protobuf.text_format
  2. $FRCN_ROOT/lib/roi_data_layer/minibatch.py 里面的约 25 行:fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image).astype(np.int)
  3. $FRCN_ROOT/lib/datasets/ds_utils.py 里面的约 12 行:hashes = np.round(boxes * scale).dot(v).astype(np.int)
  4. $FRCN_ROOT/lib/fast_rcnn/test.py 里面的约 129 行:hashes = np.round(blobs['rois'] * cfg.DEDUP_BOXES).dot(v).astype(np.int)
  5. $FRCN_ROOT/lib/rpn/proposal_target_layer.py 里面:
    1. 约 60 行:fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image).astype(np.int)
    2. 约 124 行:cls = int(clss[ind])
    3. 约 166 行:size=int(fg_rois_per_this_image)
    4. 约 177 行:size=int(bg_rois_per_this_image)
    5. 约 184 行:labels[int(fg_rois_per_this_image):] = 0

添加 CPU 支持

因为网络比较大,rbg 大神压根没想让你用 cpu 来跑。不过为了完整一点,我们还是加上 CPU 的支持吧。

在源代码的 pull-request 里面可以找到几个 cpu 的实现。经过测试,这个版本 的代码可以拿来直接使用。其他几个版本,例如 这个版本,就需要把 base_lr 设置得非常非常低,特被难以训练。

如果希望使用纯 CPU

这是个奇怪的需求…… 对…… 而且特别麻烦。也就是说,我们得剔除一些 GPU 的代码。 在 $FRCN_ROOT/lib/setup.py 中,注释掉

CUDA = locate_cuda()
self.set_executable('compiler_so', CUDA['nvcc'])
Extension('nms.gpu_nms',
    ['nms/nms_kernel.cu', 'nms/gpu_nms.pyx'],
    library_dirs=[CUDA['lib64']],
    libraries=['cudart'],
    language='c++',
    runtime_library_dirs=[CUDA['lib64']],
    # this syntax is specific to this build system
    # we're only going to use certain compiler args with nvcc and not with
    # gcc the implementation of this trick is in customize_compiler() below
    extra_compile_args={'gcc': ["-Wno-unused-function"],
                        'nvcc': ['-arch=sm_35',
                                 '--ptxas-options=-v',
                                 '-c',
                                 '--compiler-options',
                                 "'-fPIC'"]},
    include_dirs = [numpy_include, CUDA['include']]
),

$FRCN_ROOT/lib/fast_rcnn/config.py 中,将 __C.USE_GPU_NMS = True 改为 False

$FRCN_ROOT/lib/fast_rcnn/nms_wrapper.py 替换成如下代码

# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------

from fast_rcnn.config import cfg

def nms(dets, thresh, force_cpu=False):
    """Dispatch to either CPU or GPU NMS implementations."""

    if dets.shape[0] == 0:
        return []
    if cfg.USE_GPU_NMS and not force_cpu:
        from nms.gpu_nms import gpu_nms
        return gpu_nms(dets, thresh, device_id=cfg.GPU_ID)
    else:
        from nms.cpu_nms import cpu_nms
        return cpu_nms(dets, thresh)

这里再加一个小 trick:打开下面几个文件

$FRCN_ROOT/tools/test_net.py
$FRCN_ROOT/tools/train_net.py

找到

caffe.set_mode_gpu()
caffe.set_device(args.gpu_id)

改成

if args.gpu_id>=0 :
    caffe.set_mode_gpu()
    caffe.set_device(args.gpu_id)
else:
    caffe.set_mode_cpu()

打开

$FRCN_ROOT/tools/train_faster_rcnn_alt_opt.py

找到

caffe.set_mode_gpu()
caffe.set_device(cfg.GPU_ID)

改成

if cfg.GPU_ID>=0 :
    caffe.set_mode_gpu()
    caffe.set_device(cfg.GPU_ID)
else:
    caffe.set_mode_cpu()

由于 GPU_ID 是一个必须填写的参数,这样修改的话,我们只要把 GPU_ID 填写成一个负数就可以使用纯 CPU 来跑了,代码更改量比较少。虽然我知道有些地方的规定是「-1 means all」。

Let’s 编译 it !

编译 Caffe 这个坑算是跳出来了。直接按照 这个笔记 来进行编译就好了。 在这里有几点需要注意:

  1. 必须开启 USE_PYTHON_LAYER = 1,py-faster-rcnn 的有几个层是拿 Python 写的,不开启的话 一定 会出问题。
  2. Python 请使用 Python2 而不是 Python3。
  3. 如果没有升级 Caffe,那么请不要使用 CUDA8.0。
  4. 如果使用 GPU,必须使用 USE_CUDNN := 1,否则无论你显存多大,都会报 “显存溢出” 的错误。

除此之外,我们还需要在 $FRCN_ROOT/lib 里面 make 一下。这个是编译上面提到的 NMS 。如果报错,请检查一下 cudaconfig 里面的 lib64:如果是通过 apt 安装的,这个路径可能是 /usr/lib/nvidia-cuda-toolkit,如果是从 NVIDIA 官网上下载的 Cuda8.0,那么路径可能是 /usr/local/cuda

跑一下测试 Demo

这个是必须的!用来检验上面的成果。 首先下载训练好的模型

./data/scripts/fetch_faster_rcnn_models.sh

然后

./tools/demo.py

纯 CPU (--cpu)的话,应该不到五分钟就能出来结果了…… 嗯……

跑一下训练 Demo

./experiments/scripts/faster_rcnn_alt_opt.sh -1 VGG16 pascal_voc

本来想验证一下训练有没有什么错误。由于是纯 CPU 在跑,跑了 4 天,才跑了第一阶段的四分之一…… 放弃了。

使用自己的数据集进行训练

这里的「训练」其实是 fine tune。我们可以在 这篇博客 里面大致了解一下通用的 fine tune 是怎么进行的。 大致是这样的:别人写出了一个模型(比如 ABC DE),并辛辛苦苦训练出来了一个权重文件,我把模型最后几层的名字和参数改掉了(比如 ABC YZ),并且训练的时候加上了别人权重文件。那么,Caffe 在训练的时候,会发现「不对啊,模型里面有 YZ 两个层,但是权重文件里面没有着两层的权重」。这个时候,Caffe 会把 ABC 三层的权重直接加载进来,并随机初始化 YZ 两层,之后只(?)训练 YZ 两层,得到新的模型。 我们要做的就是这个事儿。

rbg 大神给出的 faster-rcnn 是在 Pascal VOC 数据集上训练和测试的,这个数据集有 20 个类别。换到我们自己的数据集上,就是一个 X 类检测问题了。比如,我只想检测人,那么就是一个 1 类检测问题。哦不对,还要记得再加上一个默认的「 背景类」。 也就是说,我们需要将模型的输出从 21(20 类 + 背景)改为 2(人 + 背景)。

修改模型文件

这里做的替换有点多,建议直接使用「查找 / 替换」功能来解决。注意,查找的时候请使用「匹配整个单词」模式。 来到 $FRCN_ROOT/models/pascal_voc/VGG16/faster_rcnn_alt_opt,打开所有文件

  1. 查找 21 (原有的 20 类 + 背景),替换为 2(人 + 背景)
  2. 查找 84 (每个框框都有 4 个坐标,21×4=84),替换成 8(2×4=8)

下面是改层的名字,但是很多教程里面都把这个给跳过去了,还能跑得溜溜的。保险起见,还是改名字吧。

来到 $FRCN_ROOT/models/pascal_voc/VGG16/faster_rcnn_alt_opt,打开所有文件

  1. 查找 cls_score,替换为 cls_score_my(也就是将层的名字改掉)
  2. 查找 bbox_pred,替换为 bbox_pred_my(也就是将层的名字改掉)

如果只是进行上述修改,我们的代码还不能跑,因为作者在写代码的时候硬编码进了 cls_scorebbox_pred 这两个层的名字。我们同样需要在代码中将上述名字替换掉。

来到 $FRCNN_ROOT/lib/fast_rcnn/,打开 train.pytest.py

  1. 查找 cls_score,替换为 cls_score_my(和上面的名字必须一样)
  2. 查找 bbox_pred,替换为 bbox_pred_my(和上面的名字必须一样)

当然,如果你想使用 faster_rcnn_end2end 或者其他的,和上面做相同的修改就行了。

准备数据

先说一下大神的代码:每个数据集有一个特定的数据读取接口,用来描述如何读 annotation 文件、如何读图片等等。这些接口通过一个 factory 来调用。 好像是什么工厂模式?《设计模式》这本书还没啃…… 有空借来啃一下。但是,为了跑一个数据集,去学工厂模式……??代价有点大。 所以大家都是把某个接口改改,形成一个新的接口,来读数据。比如 这个教程。 但是一个接口实在太大了,我脑子 Buffer 太小,照着各种教程改接口都没有成功,改着改着就乱了。 那么,我们可以使用另一种方法:做一个「假的」数据集。也就是说,把我们的数据集按照 Pascal VOC 或者 coco 的格式重新组织。

Pascal VOC 的数据集可以下载到,里面的 annotation 都是 xml 格式的,和我拿到的数据标注不太一样。用 Python 写一个数据转换脚本就可以了。

我拿到的 ground truth 是这样的:

图片编号:[x1,y1,x2,y2][x1,y1,x2,y2]........

举个例子:

120:[171,68,232,129][392,90,463,171]
150:[403,85,470,159][443,71,490,133][217,75,273,139]

自己写了一个脚本,来将这个 ground truth 转换成 Pascal VOC 样式的 xml 数据格式,并自动生成 train 集、val 集 test 集和 train_val 集(这个是干啥的?)。就是把一个对象转成 xml。因为数据标注有错误,可能越界,所以加了一点范围的规定。assert 用来 debug。

注意顺序一定是「左上 – 右下」。如果不确定的话也可以加一点代码,把框框在图片中框出来看看效果

找到 $FRCN_ROOT/lib/datasets/pascal_voc.py,做如下修改:

  1. self._classes,里面改成你自己的类别。(我只有一类,那么就是 self._classes = ('__background__','person')。需要特别注意背景类!)
  2. self._image_ext 文件后缀名

如果使用的是 train_faster_rcnn_alt_opt,在 $FRCN_ROOT/tools/train_faster_rcnn_alt_opt.py 中有一个 max_iters,需要根据数据量进行修改四个阶段的迭代次数。好像设置到「正好能跑完 1 代 / 2 代」就可以了,但是在下面的 train_fast_rcnn 中,我看到了 cfg.TRAIN.IMS_PER_BATCH = 2,所以__感觉__max_iters 中的所有数值都要除以 2。(对么?求指教)

更正: 第一和第三步的 batch_size=1,第二和第四步的 batch_size=2。默认是 VOC 数据集跑 16 代。

开始跑!

上面写了这么多,终于可以开始跑了。

./experiments/scripts/faster_rcnn_alt_opt.sh -1 VGG16 pascal_voc

我是拿纯 CPU 跑的,所以里面你可以看到一个 -1。如果有 GPU 的话,写上 GPU 的 编号 就可以了。训练过程中可以看到 loss。对于第一阶段(因为数据比较整齐),我还写了一个暴力的 loss 可视化脚本

Loss 可视化 生成的权重文件在 $FRCN_ROOT/output 里。 如果数据标错了,例如不是按照「左上 – 右下」的顺序标的,那么需要删除 $FRCN_ROOT/data/cache 里面的 Cache。这个 Cache 不知道是干什么用的。

错误处理

  1. 如果在最后出现 KeyError: 'xxxxxxxxxx',请删除 $FRCN_ROOT/data/VOCdevkit2007/annotations_cache/annots.pkl
  2. 如果中途发现标错了数据,重新标注数据后,请删除 $FRCN_ROOT/data/cache/voc_2007_trainval_gt_roidb.pkl
  3. 如果最后测试出现 IndexError: too many indices for array,那是因为你的测试数据中缺少了某些类别。请根据错误提示,找到对应的代码($FRCN_ROOT/lib/datasets/voc_eval.py 第148行),前面加上一个if语句:
    if len(BB) != 0:
    BB = BB[sorted_ind, :]

写在最后

感谢你能看完。如果你有任何疑问,请在下面评论区留言。博客流量小,所以几乎都会回复的。

参考资料


评论

  1. 靳小强 的头像
    靳小强

    我的也是KeyError: ‘000000’,但是没有你所说的那个文件

    1. 小金鱼儿 的头像
      小金鱼儿

      这个我也是对照着代码找的解决方案。别再用Caffe的版本了……PyTorch自带的不好嘛~

  2. XieXY 的头像
    XieXY

    博主你好,我在训练的时候也遇到了IndexError: too many indices for array这个问题,按照您的方法修改了之后能运行成功,我想知道使用if会不会是跳过了错误而没有解决呢?
    还有一个问题,我运行的AP值也非常低,几乎接近0。我训练了有一万次,应该不存在训练次数不够的问题,并且我查看了log文件loss值不是NAN,但是不收敛。
    期待您的回复。

    1. 小金鱼儿 的头像
      小金鱼儿

      不会的,放心好了。大家都是这么改的😂
      Learning Rate改小/改大一点试试?或者看看是不是预训练模型没有加载?或者看看你的 model cfg 文件,是不是把整个网络给固定住了?

  3. download 的头像
    download

    你好,我程序跑通后,所有类别的AP都为0,不知道哪里出错了,希望博主能提供一些思路,谢谢博主!!

    1. 小金鱼儿 的头像
      小金鱼儿

      看看训练Log,是不是Loss为Nan了?
      如果是的话,检查一下坐标范围(坐标是从0开始到n-1,还是从1开始到n?)

  4. lemon 的头像
    lemon

    你好,假如微调后面几层网络,那训练时,怎么冻结前面几层网络,是将其学习率设置为0吗?

    1. 小金鱼儿 的头像
      小金鱼儿

      是的

  5. Flames 的头像
    Flames

    你好,我成功用end2end2方法跑了,但是在跑alt方法2时,到stage2(即第一次训练Faster RCNN)时创建的第一个data层就失败了,我查debug了下是因为cfg.HAS_RPN是true,而他应在train_faster_rcnn()中被赋值为false了(我用print也确实是这样),但是!!到layer.py中cfg.HAS_RPN又变成true了!!难道这几个函数调用的时候运行空间会变?可很多人没这个问题啊……求博主解答。
    我是win10+cuda9.2+cudnn7.1+caffe

    1. 小金鱼儿 的头像
      小金鱼儿

      表示并没有遇到过这个问题

  6. 张酱油。 的头像
    张酱油。

    你好,我遇到了一些问题,就是我的ap特别的低下,不知道为什么。谢谢你,看了你的文章后,不知道怎么回事心里比较舒服。

    1. 小金鱼儿 的头像
      小金鱼儿

      ap很低的话,建议看一下是不是训练次数不够。我是大概训练了十几个epoch。

      新学到一个方法: 找四五张图片,拼命训练,让它严重过拟合,之后再用大数据集进行训练。——我正在尝试。

    2. 张酱油。 的头像
      张酱油。

      我也是这么想的,我制作的训练集都是相似图片,但不知道为什么还是低,刚8000 4000 8000 4000才不到0.3。方便加个q吗,690780412感谢你的回复!姓名验证随便写就好。

  7. Leerw 的头像
    Leerw

    楼主你好,我现在已经成功地训练出来了,下面是我的问题,在GitHub上问了一下,所以用的是英文,训练的结果是AP for background为0,这是正常的还是我漏掉了某一步呢?我上网查了一下,CSDN有个博客说faster rcnn 会自己生成background,但是我也不知道他说得对不对,还是说我们准备自己数据集的时候还要加上background的图片呢?
    Hi! After my training on my own dataset, it said my background AP is 0 and my interest class AP is 0.905, so I wanna know why the background AP is 0 and does it matter?
    Here is the text:
    AP for backgroud = 0.0000
    AP for myclass = 0.9025
    Mean AP = 0.4513

    Results:
    0.000
    0.903
    0.451
    
    1. 小金鱼儿 的头像
      小金鱼儿

      我们自己不需要准备Background数据的。

      按照论文中说的,生成框框之后,“将与 某个 GT的IoU大于XX的框认定为正样本的某个类,将与 所有 GT的IoU均小于XX的框认定为负样本(即背景类)”

      所以如果是做只有一个分类的检测问题(例如,人),只需要把需要检测的物体框出来就好了。

      在代码中,默认第0类是背景类,准备数据的时候,我们自己定义的类别需要从1开始。

      但我怎么不记得最后输出的时候还会输出Background?

  8. David 的头像
    David

    正好今天看到了,回答其中的一个问题:

    自己写了一个脚本,来将这个 ground truth 转换成 Pascal VOC 样式的 xml 数据格式,并自动生成 train 集、val 集 test 集和 train_val 集(这个是干啥的?)

    解释如下:

    1. test.txt是测试集,大概占整个数据集的20%;
    2. trainval.txt是训练集和验证集的组合,也就是整个数据集剩下的80%;
    3. train.txt是训练集,是trainval.txt的90%;
    4. val.txt是验证集,是trainval.txt的10%。
    1. 小惟 的头像
      小惟

      想问一下划分后的这四个集合具体都用在模型中哪里了呢?训练后得到的AP是基于这四个里面哪一个集合上的结果得到的呢?小白不太看得懂代码,来请教一下,谢谢

    2. 小金鱼儿 的头像
      小金鱼儿

      四个集合?我代码里好像只划分了train和eval吧

  9. Leerw 的头像
    Leerw

    请问训练到最后出现了博主所说的IndexError: too many indices for array错误,我如何能够改正之后继续训练呢?

    1. 小金鱼儿 的头像
      小金鱼儿

      时间太长有点忘记了

      依稀记得可能是以下两种之一:

      1. 类别编号是从0开始还是从1开始?
      2. 例如有5类,那么第5类在测试数据里面没有出现

      解决办法其实这里面也说了:

      $FRCN_ROOT/lib/datasets/voc_eval.py第 148 行,前面加上一个 if 语句。

  10. hito 的头像
    hito

    谢谢大神不耐其烦的回答,真的感谢。通过google总算找到你的那个所有代码了,

    def vis(img, bbgt, bb, yes, no):
        # 可视化
        image_data = img_as_float(io.imread(img))
        # 蓝色是GroundTruth
        for b in bbgt:
            rr, cc =  draw.polygon_erimeter([int(b[1]), int(b[1]), int(b[3]), int(b[3])],[int(b[0]), int(b[2]), int(b[2]), int(b[0])])
            draw.set_color(image_data, (rr, cc), (0, 0, 1))
          for ith, b in enumerate(bb):
              rr, cc = draw.polygon_perimeter([int(b[1]), int(b[1]), int(b[3]), int(b[3]),[int(b[0]), int(b[2]), int(b[2]), int(b[0])])
    

    这里draw.polygon_erimeter总是出错,说AttributeError: 'module' object has no attribute 'polygon_perimeter',这是什么情况?我的python是2.7,我找了很久也没有找到相关解法,还有polygon这[int(b[1]), int(b[1]), int(b[3]), int(b[3])]四个点,这能绘制出图形吗?没有理解为什么是这样?

    1. 小金鱼儿 的头像
      小金鱼儿

      我是在Python3下面执行成功的。不保证其他环境可以执行。

      前面四个是x坐标,后面四个是y坐标,画出一个多边形。

  11. hito 的头像
    hito
    1. 自己写了一个脚本. 来将这个 ground truth 转换成 Pascal VOC 样式的 xml 数据格式,
    2. 注意顺序一定是「左上 – 右下」。如果不确定的话也可以加一点代码,把框框在图片中框出来看看效果。

    亲,你这两处给的链接我打开了好多次都没有打开,能再给一个链接吗?

    再问大神一个问题,我的目标由于比较小,小的话大概是10×10,大的话大概有50×50,现在训练的结果很不好,有什么方法可以改善吗?

    1. 小金鱼儿 的头像
      小金鱼儿

      关于网站打不开的问题……你可以使用某些高科技软件进行浏览……你懂的。

      我不知道为什么github可以使用,但是gist就不能使用。为了做备份,我把我使用到的所有脚本都放进去了,里面的代码可能比较多。而且你可能还需要进行小幅修改以适应你自己的情况。

      如果目标比较小,你需要改一下代码:在rpn中,有一个步骤是将小于$16*16$的候选框全部剔除。请在$FRCNN_ROOT/lib/rpn/proposal_layer.py里面进行修改。不想改这里的代码的话可以改一下配置文件中的cfg[cfg_key].RPN_MIN_SIZE

  12. hito 的头像
    hito

    $FRCNN_ROOT/lib/fast_rcnn/,打开 train.pytest.py

    查找 cls_score,替换为 cls_score_my(和上面的名字必须一样)
    查找 bbox_pred,替换为 bbox_pred_my(和上面的名字必须一样)

    这里修改了bbox_pred之后会出错,提示keyerror,但是如果不修改的话就不会有问题,请问这里是不是不改,你是不是弄错了?

    1. 小金鱼儿 的头像
      小金鱼儿

      改层的名字是可选的。如果选择“改”,那么Caffe会随机初始化这些层,然后开始训练。如果选择“不改”,我不太清楚会发生什么。

      实际测试中,我发现做不做这一步好像影响不大。

      具体做法见这篇文章,下面提到了应该如何进行修改。

  13. David 的头像
    David

    建议参照这篇博客Py-faster-rcnn实现自己的数据train和demo,对照看看哪里出现问题了。

    1. 小金鱼儿 的头像
      小金鱼儿

      谢谢超哥,最后发现是转的标注出了问题

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注