前面写了将 Faster R-CNN 用在自己的数据集上,这次来写一下RCNN的环境搭建和训练过程。
深坑,请注意。
为什么说是坑呢?
首先,它是Matlab的……没有歧视Matlab的意思,只是前面都是在py,突然换成Matlab很不适应。
其次,里面使用的Caffe是一个非常早的v0.999版本。里面的各种用法都和现在版本的不一样!
再次……遇到问题,搜出来的都是py-faster-rcnn的处理办法……
虽然官方README里面写得比较清楚了,但还是遇到不少麻烦。
好吧,不吐槽,开工。
源代码的获取
这个不多说,git clone https://github.com/rbgirshick/rcnn
就好了。
然后,下载Caffe v0.999,放到 $RCNN_ROOT/external
里面,改名为 caffe
。
编译Caffe
首先,修改一些代码:
找到 Makefile
文件,大约266行左右关于Matlab的说明段,整体替换成如下代码(必须使用Tab缩进):
$(MAT$(PROJECT)_SO): $(MAT$(PROJECT)_SRC) $(STATIC_NAME)
@ if [ -z "$(MATLAB_DIR)" ]; then \
echo "MATLAB_DIR must be specified in $(CONFIG_FILE)" \
"to build mat$(PROJECT)."; \
exit 1; \
fi
$(MATLAB_DIR)/bin/mex $(MAT$(PROJECT)_SRC) \
CXXFLAGS="\$CXXFLAGS $(CXXFLAGS) $(WARNINGS)" \
CXXLIBS="\$CXXLIBS $(STATIC_NAME) $(LDFLAGS)" -output $@
@ echo
这样就能解决“错误使用 -o 参数”和其他关于链接的问题。
然后,找到 $CAFFE_ROOT/include/caffe/util/math_functions.hpp
,将
using std::signbit;
DEFINE_CAFFE_CPU_UNARY_FUNC(sgnbit, y[i] = signbit(x[i]));
改为
// using std::signbit;
DEFINE_CAFFE_CPU_UNARY_FUNC(sgnbit, y[i] = std::signbit(x[i]));
这样能解决一个很奇怪的编译问题。
如果你的GPU比较老(capability<3),那么即使顺利通过编译,运行的时候也会直接报错的。修改方法如下:
找到 $CAFFE_ROOT/include/caffe/common.hpp
,在第162行左右找到 CAFFE_GET_BLOCKS
函数,改成
// CUDA: number of blocks for threads.
inline int CAFFE_GET_BLOCKS(const int N) {
int blocks = (N + CAFFE_CUDA_NUM_THREADS - 1) / CAFFE_CUDA_NUM_THREADS;
return std::min(blocks,65535);
}
最后,按照这个文章里的记录进行配置和编译。我们需要编译 all
、test
、pycaffe
、matcaffe
。
如果你的GPU比较新,可以把 CUDA_ARCH
(我也不知道这个东西是干啥的)设置稍微高一点。例如,在GTX1060上,我用的是
CUDA_ARCH := -gencode arch=compute_20,code=sm_20 \
-gencode arch=compute_20,code=sm_21 \
-gencode arch=compute_30,code=sm_30 \
-gencode arch=compute_35,code=sm_35 \
-gencode arch=compute_50,code=sm_50 \
-gencode arch=compute_52,code=sm_52 \
-gencode arch=compute_60,code=sm_60 \
-gencode arch=compute_61,code=sm_61 \
-gencode arch=compute_61,code=compute_61
不报错。在GTX540上,只能用到52。
当然,跑test是有一定概率出错的,并且100%通不过全部测试。忽略吧……
第一仗 编译 结束。
Matlab准备
这个,照着README来就好了。
从 $RCNN_ROOT
里面启动matlab,会出现 R-CNN startup done
。如果没有,那么手动运行一下 >> startup
。
然后是自动下载和编译依赖库,只需要我们 >> rcnn_build()
一下就好了。一般不会出问题。
最后进行一个小测试: >> key = caffe('get_init_key')
,如果结果是 -2
,恭喜。
第二仗 Matlab,轻松结束。
跑Demo
这个也要按照README来。
进入 $RCNN_ROOT
执行./data/fetch_models.sh
获取训练好的Model。如遇到网络问题,请挂梯子。
执行./data/fetch_selective_search_data.sh
获取在ImageNet上使用SelectiveSearch提取出来的Window信息(这个不知道是干啥的)。如遇到网络问题,请挂梯子。
启动Matlab,执行 rcnn_demo
第三仗 Demo ,轻松结束。
Fine Tune 自己的数据
简直了!
准备数据
可以按照将 Faster R-CNN 用在自己的数据集上来准备数据。也就是说,用PascalVOC的壳,里面装的是自己的数据。
RCNN需要依赖VOCdevkit里面的一些文件的。我们也需要对这些文件进行一些修改。找到 VOCdevkit/VOCcode/VOCinit.m
,将里面的类别标签替换成你自己的。同时,如果你的图片是PNG格式的,需要在这个文件中查找 jpg
,替换成 png
。
由于这篇文章中使用的是Python,比较灵活,做Annotation的时候一些细节没有考虑到,导致后面会出错。如果你在后面的训练过程中发现“网络加载完毕就崩溃”,那么可以看看这里:
VodDevkit里面会将xml转为Matlab Struct,而转换用的代码是非常……额……原始……的。XML稍有问题,就会死给你看。比较容易出错的地方有:
- XML文件头部,不能有
<?xml version="1.0" ?>
记号 - 使用LabelImage标注的话,不能有
verified="no"
或者verified="yes"
记号 - 图片中没有物体,会报错
- 必须设置
folder
为VOC2007
- 有多余空格会报错
所以,我们需要做两个修改:
VOCdevkit/VOCcode/VOCxml2struct.m
中,将xml(xml==9|xml==10|xml==13)=[];
改为xml(xml==9|xml==10|xml==13|xml==' ')=[];
VOCdevkit/VOCcode/VOCreadxml.m
中,改为下面的代码
function rec = VOCreadxml(path)
if length(path)>5&&strcmp(path(1:5),'http:')
xml=urlread(path)';
else
f=fopen(path,'r');
xml=fread(f,'*char')';
fclose(f);
xml = strrep(xml,'<?xml version="1.0" ?>','');
xml = strrep(xml,'fooImages','VOC2007');
xml = strrep(xml,' verified="no"','');
xml = strrep(xml,' verified="yes"','');
end
rec=VOCxml2struct(xml);
或者,我们可以借鉴一下LabelImage的代码,将Annotation再进行一次转换。由于长度原因,代码我放gist上面了,有两个文件,分别是pascal_voc_io.py和voc_annotation_correct.py。经过转换后,我们的RCNN就会听话许多。
进行训练
训练分为四步: 计算候选区域、CNN的Fine-Tune、提取特征、训练SVM
计算候选区域
如果已经运行过 R-CNN 的 demo,那就一定执行过
rcnn/data
下的fetch_selective_search_data.sh
脚本,在selective_selective_data
文件夹下就一定有一系列的.mat
文件,这就是作者预先计算好的候选区域信息(实际上是候选区域的坐标)。如果不自行计算候选区域,下一步提取特征的时候就会使用voc_2007_*.mat中储存的候选区域信息,会使结果出错。
所以,应对方法是…… 删除!
cd ./data/selective_search_data
rm voc_2007_train.mat
rm voc_2007_val.mat
rm voc_2007_trainval.mat
rm voc_2007_test.mat
然后我们还需要再根据自己的数据集来计算这些文件。这里对这篇博客中的代码进行了一些修改:
function selective_search_boxes_generator(chunk)
mat_file = sprintf('./data/selective_search_data/voc_2007_%s.mat', chunk);
if exist(mat_file, 'file')
fprintf('File exists\n');
return
end
% image_ids = load(sprintf('./datasets/VOCdevkit2007/VOC2007/ImageSets/Main/%s.txt', chunk));
image_ids = textread(sprintf('./datasets/VOCdevkit2007/VOC2007/ImageSets/Main/%s.txt', chunk),'%s');
image_num = size(image_ids, 1);
images = cell(1, image_num);
boxes = cell(1, image_num);
fprintf('Computing candidate regions...\n');
th = tic();
fast_mode = true;
im_width = 500;
parfor i = 1 : image_num
image_id = cell2mat(image_ids(i));
images{1, i} = image_id;
image_file = sprintf('./datasets/VOCdevkit2007/VOC2007/JPEGImages/%s.png', image_id);
im = imread(image_file);
box = selective_search_boxes(im, fast_mode, im_width);
% Rounding is necessary, or 'loss' will always be '0' when fine-tuning.
boxes{1, i} = round(box);
fprintf('%d / %d %s.png\n', i,image_num,image_id);
end
fprintf('Finished, costs %.3fs\n', toc(th));
fprintf('Saving MAT file to %s...\n', mat_file);
save(mat_file, 'images', 'boxes');
fprintf('Done\n');
将上述文件保存成 selective_search_boxes_generator.m
,然后启动Matlab,执行
selective_search_boxes_generator('train');
selective_search_boxes_generator('val');
selective_search_boxes_generator('trainval');
selective_search_boxes_generator('test');
下面是照着官方教程来的:
继续在Matlab里面执行
imdb_trainval = imdb_from_voc('datasets/VOCdevkit2007', 'trainval', '2007');
imdb_test = imdb_from_voc('datasets/VOCdevkit2007', 'test', '2007');
rcnn_make_window_file(imdb_trainval, 'external/caffe/examples/pascal-finetuning');
rcnn_make_window_file(imdb_test, 'external/caffe/examples/pascal-finetuning');
以获取“Window File”。执行完成后,建议去 $CAFFE_ROOT/examples/pascal-finetuning/
里面看看 window_file_voc_2007_test.txt
和 window_file_voc_2007_trainval.txt
两个文件。
这两个文件的格式是这样的:
# image_index
img_path
channels
height
width
num_windows
class_index overlap x1 y1 x2 y2
其中如果 class_index
是0的话,说明这个框框是背景类。如果 class_index
全部都是0,请照着上面的说明转一下标注文件。
修改网络配置
pascal_finetune_solver.prototxt
- 调整迭代次数
max_iter
(作者说在Pascal VOC 2012 上训练,4w iter就收敛了) - 调整每次测试数量
test_iter
(别太大) - 调整测试频率
test_interval
(别太频繁) - 调整快照频率
snapshot
(别太频繁)
- 调整迭代次数
pascal_finetune_train.prototxt
- 305行左右的
num_output
:改成你的类别数+1 - 如果报显存不足的话,调整
batch_size
- 305行左右的
pascal_finetune_val.prototxt
- 305行左右的
num_output
:改成你的类别数+1 - 如果报显存不足的话,调整
batch_size
- 305行左右的
最理想状态是 max_iter/test_interval*test_iter=你的测试图片总数
。
好像不用修改层的名字。
训练特征提取网络
进入 $CAFFE_ROOT/examples/pascal-finetuning
GLOG_logtostderr=1 ../../build/tools/finetune_net.bin pascal_finetune_solver.prototxt <到rcnn文件夹的路径>/rcnn/data/caffe_nets/ilsvrc_2012_train_iter_310k 2>&1 | tee log.txt
训练完毕后,在当前文件夹下会生成一大堆 pascal_finetune_train_iter_XXXX
和 pascal_finetune_train_iter_XXXX.solverstate
文件,这就是我们的快照。数值最大的那个就是我们的最终模型。
选择一个不带.solverstate
的文件,改名为 finetune_voc_2007_trainval_iter_70k
,复制到 $RCNN_ROOT/data/caffe_nets
下备用。
回顾训练过程
这个版本的Caffe提供了一些Log可视化工具,位于 $CAFFE_ROOT/tools/extra
。使用前需要将这里面的.py
、.sh
、.example
文件加上运行位(chmod +x
)。
将 $CAFFE_ROOT/examples/pascal-finetuning/log.txt
复制到 $CAFFE_ROOT/tools/extra
里面。执行./parse_log.sh log.txt
,然后执行
./plot_training_log.py.example <曲线类型参数> <图片文件名>.png log.txt
其中曲线类型可以为(请把前面的序号-1……我偷懒不想打表格)
- Test accuracy vs. Iters
- Test accuracy vs. Seconds
- Test loss vs. Iters
- Test loss vs. Seconds
- Train learning rate vs. Iters
- Train learning rate vs. Seconds
- Train loss vs. Iters
- Train loss vs. Seconds
想问这里的Test Accuracy是什么鬼?为什么这么高?
进行特征提取
回到 $RCNN_ROOT
,进入Matlab,执行
rcnn_exp_cache_features('train');
rcnn_exp_cache_features('val');
rcnn_exp_cache_features('test_1');
rcnn_exp_cache_features('test_2');
程序分别将train、val、test前半部分和后半部分的图片送入网络,提取pool5层的输出作为特征,最后保存到硬盘上。生成文件非常多,速度非常满。不仅会用到GPU,而且由于代码中使用了 parfor
,CPU大多也是慢负荷运转的。
速度有多慢呢?
rcnn: cache features: 213/900
[features: 6.928s]
[saving: 2.215s]
[avg time: 9.793s (total: 2076.135s)]
这里遇到了一个坑: 我们的数据集中,不能保证每张图片上面都有目标物体……好像是会导致bbox坐标出现 NaN NaN NaN NaN
的奇葩状况。为了解决这个问题,我对 $RCNN_ROOT/rcnn_extract_regions.m
做了一点小修改,在此函数的最前面加入了一句 boxes(isnan(boxes))=0;
。这样做能解决掉上述问题,但是不知道对后续流程有没有什么影响。
训练SVM
最后还要再使用liblinear训练一个分类器嘛……
test_results = rcnn_exp_train_and_test()
一句话就能解决了。如果liblinear出错,请删除 $RCNN_ROOT/liblinear_train.mexa64
,到 $RCNN_ROOT
里面打开Matlab,运行 >> rcnn_build()
重新编译一下liblinear即可。
在测试结束时,程序绘制出precision/recall曲线和ap。precision/recall曲线保存在 $RCNN_ROOT/cachedir/voc_2007_test/face_pr_voc_2007_test.jpg
,以方便没有界面的苦逼孩子观看。同时,训练好的模型被保存在了 $RCNN_ROOT/cachedir/voc_2007_trainval/rcnn_demo.m
里。
好像……好像结果不怎么好的样子……
还有,我知道什么是Recall,什么是Accuracy,但是这里的Recall-Accuracy曲线就不明白了。
后续
由于整个过程太长了,所以写了个自动脚本。在这里。如有需要,请修改后使用。比如,第一步删文件要不要那么狠,第三步替换一下你的迭代次数。
总结
第一次感受到快被坑哭了是什么感觉。莫名其妙地各种报错,报各种莫名其妙的错,最后AP也是超级低。
环境折腾小一周,训练折腾小一周……十分感谢这个博主,这是我能找到的最全面的一个RCNN使用笔记了,使我少跳了许多坑。
发表回复