R-CNN,全称是Region Convolutional Neural Network,也就是区域卷积神经网络,所谓区域,是在一个图片中提取出多个候选区域(Region Proposal),认为这些候选区域中可能包含物体,也就是和ground truth有交集,从这些候选区域中选出好的那些区域也就得到了我们的预测结果。
R-CNN在一个目标检测的公认数据集Pascal VOC上取得了卓越的效果,比VOC 2012上的最好结果提升了30%,达到了53.3%的mAP。
参考完整学习目标检测中的 Recalls, Precisions, AP, mAP 算法 Part1 - 史蒂芬方的文章 - 知乎
https://zhuanlan.zhihu.com/p/79186684
由于目标检测中包含了多个类别,所以我们要分别对每个类别去计算AP(Average Precision),然后对所有类别取平均,也就得到了mAP(mean Average Precision)。
我们先考虑一个类class1,计算class1的AP:
就这样,我们得到了class1的AP,那么我们对所有的类别取平均即可得到mAP。
R-CNN是基于卷积神经网络的,更具体地说,是基于AlexNet的。R-CNN的架构主要分成了4个部分:
输入一张图片,要得到可能包含物体的候选框,最直接的办法是穷举,把所有可能的框都枚举出来,但是这样会得到数量非常大的候选框,使得模型的效率及其低下。更好的办法是一些启发式的搜索,比如选择化搜索Selective Search,能够帮助我们更高效地提取出候选框。
实现的时候考虑使用opencv库:
def get_selective_search():
gs = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()
return gs
在得到候选框之后,我们将这每一个候选框都输入一个卷积神经网络进行特征提取,这里需要注意的是我们的候选框形状是不定的,如果要输入AlexNet,那么必须是227*227的图片,所以这里需要使用一些变换把图片缩放成固定大小的。
注:训练的时候,AlexNet最后一层的神经元个数是类别数+1(1是背景,也就是不包含物体)。之后输入到SVM的时候需要把AlexNet的最后一层去掉,只要最后一个全连接层,因为我们并不是让CNN来完成分类的任务,而是需要得到网络提取的特征。(原因在后面)
实现的时候考虑使用pytorch的transforms
:
transform = transforms.Compose([
transforms.ToPILImage(),
transforms.Resize((227, 227)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
训练阶段的第一个问题就是数据从哪里来?实际上,我们训练卷积神经网络是为了判断图片是否有物体以及是哪一类物体,而不是直接得到bounding box的位置,所以我们的训练数据并不是原始的图片,而是经过选择化搜索得到的候选区域。
因此我们先使用选择化搜索得到候选区域以及候选区域对应的类别(包括背景),对于每一个候选区域,我们认为和ground truth的IoU>0.5的为正例(包含物体),反之则为反例。这样就得到了训练AlexNet的数据集。
在训练时,我们使用微调(fine tune),先使用VOC的数据集对模型进行预训练,然后再用自己的数据集进行训练,这样能够大大加快训练的效率。
实现时我们直接使用pytorch中预训练的alexnet模型:
model = models.alexnet(pretrained=True)
使用AlexNet提取到4096维的特征之后,我们训练n个线性的SVM,其中n为类别数,然后将特征输入n个SVM来进行分类(采用的是One versus Many来使SVM能够解决多分类问题)。
问题:为什么不直接用CNN最后一层的softmax来进行分类,而使用SVM?
因为CNN实际上做的事情是特征提取,为了避免CNN过拟合,所以选择了IoU>0.5作为threshold来增加样本数量避免过拟合,而这样的IoU会导致框的位置不准确。而SVM适合小样本的训练,因此选择了IoU>0.7作为threshold,可以使得框位置更加精确。
这里使用了 难反例挖掘(hard negative mining) 用来解决正反例数量不平衡的问题,由于我们的数据中,反例占大多数,也就是不包含物体的候选框,这时候训练出来的SVM可能效果不好,因为他即便全部都识别为反例,也能达到较高的精度,但是也导致了非常低的召回率。
所谓难反例挖掘,就是找到一些难判断的反例(hard negative)输入到模型中参与训练,这样会比使用大量的easy negative要效果好。
那么如何找到难的反例呢?
一开始我们会使用一个初始的反例集合(一部分的反例)去训练,训练一轮之后,我们就用剩余的反例去进行测试,选取其中概率最大的(最容易被判断为正例的)加入到负样本集中重新进行训练。
在测试阶段,我们首先对一张图片进行选择化搜索,然后将得到的候选区域输入到模型中,如果包含物体,那么就可以利用SVM计算出包含物体的概率。由于这样得到的很多框是相似的,框住的其实是同一个物体,所以我们要去除这些冗余的框,也就是使用非极大值抑制(Non-Maximum Suppression)。
具体的做法是将候选区域按照包含物体的概率进行排序,每一次找出概率最大的那个,然后跟剩余的计算IoU,如果IoU大于某个阈值比如0.3,就剔除掉这些候选框。
def nms(rect_list, score_list):
nms_rects = list()
nms_scores = list()
rect_array = np.array(rect_list)
score_array = np.array(score_list)
# 一次排序后即可
# 按分类概率从大到小排序
idxs = np.argsort(score_array)[::-1]
rect_array = rect_array[idxs]
score_array = score_array[idxs]
thresh = 0.3
while len(score_array) > 0:
# 添加分类概率最大的边界框
nms_rects.append(rect_array[0])
nms_scores.append(score_array[0])
rect_array = rect_array[1:]
score_array = score_array[1:]
length = len(score_array)
if length <= 0:
break
# 计算IoU
iou_scores = util.iou(np.array(nms_rects[len(nms_rects) - 1]), rect_array)
# 去除重叠率大于等于thresh的边界框
idxs = np.where(iou_scores < thresh)[0]
rect_array = rect_array[idxs]
score_array = score_array[idxs]
return nms_rects, nms_scores
通过非极大值抑制,我们就去除了冗余的候选框,从而得到了比较好的预测结果。
RCNN论文中还提到了边界框回归这一技术,在之后的目标检测论文中也十分常见。详情参考这篇文章https://blog.csdn.net/zijin0802034/article/details/77685438/。