Faster-RCNN深度剖析+源码debug级讲解系列(二)Classifier对ROI进行回归

前言

在上一篇中我们详细的debug了Faster-RCNN的RPN网络,以及如何依据RPN对anchor进行第一次的坐标偏移。值得注意的是整个过程都是在图像的原始比例上进行的,而不是在stride层面。RPN网络的输出参数是直接调整原始anchor坐标的。
在这篇文章中我们会继续debug网络的分类部分,即模型是如何基于ROI的输出结果进行分类和回归的。当然,没有看过上一篇的小伙伴可以先看下上篇,传送门如下:
Faster-RCNN深度剖析+源码debug级讲解系列(一)RPN网络和Bbox回归

源码debug

我们首先回到上次nets/frcnn.py源码:

class FasterRCNN(nn.Module):
    def __init__(self, num_classes, 
                    mode = "training",
                    feat_stride = 16,
                    anchor_scales = [8, 16, 32],
                    ratios = [0.5, 1, 2],
                    backbone = 'vgg'):
        super(FasterRCNN, self).__init__()
        self.feat_stride = feat_stride
        if backbone == 'vgg':
            self.extractor, classifier = decom_vgg16()
            
            self.rpn = RegionProposalNetwork(
                512, 512,
                ratios=ratios,
                anchor_scales=anchor_scales,
                feat_stride=self.feat_stride,
                mode = mode
            )
            self.head = VGG16RoIHead(
                n_class=num_classes + 1,
                roi_size=7,
                spatial_scale=1,
                classifier=classifier
            )
        elif backbone == 'resnet50':
            self.extractor, classifier = resnet50()

            self.rpn = RegionProposalNetwork(
                1024, 512,
                ratios=ratios,
                anchor_scales=anchor_scales,
                feat_stride=self.feat_stride,
                mode = mode
            )
            self.head = Resnet50RoIHead(
                n_class=num_classes + 1,
                roi_size=14,
                spatial_scale=1,
                classifier=classifier
            )
            
    def forward(self, x, scale=1.):
        img_size = x.shape[2:]
        base_feature = self.extractor(x)
        _, _, rois, roi_indices, _ = self.rpn(base_feature, img_size, scale)
        roi_cls_locs, roi_scores = self.head(base_feature, rois, roi_indices, img_size)
        return roi_cls_locs, roi_scores, rois, roi_indices

我们上次完整的debug和分析了rpn网络的输出。假设原图是800*800尺寸,16倍下采样之后是50*50。假设有9个anchor,那么每张图片的rpn输入和输出就是50*50*9=22500个框。然后根据输出的包含object的置信度排序,默认剩余12000个框,再通过NMS处,最终再按照置信度输出600个框。这就是单次pn的最终输出。因为BatchSize张图片,所以输出的框是K=600*BatchSize。假设这时候的框的数量为K(1200 eg.),那么:

roi_cls_locs, roi_scores = self.head(base_feature, rois, roi_indices, img_size)

这里rois和roi_indices的shape分别是(K,4)和(K)。

            self.head = Resnet50RoIHead(
                n_class=num_classes + 1,
                roi_size=14,
                spatial_scale=1,
                classifier=classifier
            )

这里的K个框并非完全输入到了分类器,而是在训练的时候对正负样本进行了采样,默认采样128个,并最终送入了Resnet50RoIHead类,也就是说实际上每个batch的样本都来自于同一个图片,这部分的代码在后面训练的部分我们再解释,这里先看Resnet50RoIHead这个负责最终分类回归的类:

class Resnet50RoIHead(nn.Module):
    def __init__(self, n_class, roi_size, spatial_scale, classifier):
        super(Resnet50RoIHead, self).__init__()
        self.classifier = classifier
        #--------------------------------------#
        #   对ROIPooling后的的结果进行回归预测
        #--------------------------------------#
        self.cls_loc = nn.Linear(2048, n_class * 4)
        #-----------------------------------#
        #   对ROIPooling后的的结果进行分类
        #-----------------------------------#
        self.score = nn.Linear(2048, n_class)
        #-----------------------------------#
        #   权值初始化
        #-----------------------------------#
        normal_init(self.cls_loc, 0, 0.001)
        normal_init(self.score, 0, 0.01)

        self.roi = RoIPool((roi_size, roi_size), spatial_scale)

    def forward(self, x, rois, roi_indices, img_size):
        n, _, _, _ = x.shape
        if x.is_cuda:
            roi_indices = roi_indices.cuda()
            rois = rois.cuda()

        rois_feature_map = torch.zeros_like(rois)
        rois_feature_map[:, [0,2]] = rois[:, [0,2]] / img_size[1] * x.size()[3]
        rois_feature_map[:, [1,3]] = rois[:, [1,3]] / img_size[0] * x.size()[2]

        indices_and_rois = torch.cat([roi_indices[:, None], rois_feature_map], dim=1)
        #-----------------------------------#
        #   利用建议框对公用特征层进行截取
        #-----------------------------------#
        pool = self.roi(x, indices_and_rois)
        #-----------------------------------#
        #   利用classifier网络进行特征提取
        #-----------------------------------#
        fc7 = self.classifier(pool)
        # 当输入为一张图片的时候,这里获得的f7的shape为[300, 2048]
        fc7 = fc7.view(fc7.size(0), -1)

        roi_cls_locs = self.cls_loc(fc7)
        roi_scores = self.score(fc7)
        roi_cls_locs = roi_cls_locs.view(n, -1, roi_cls_locs.size(1))
        roi_scores = roi_scores.view(n, -1, roi_scores.size(1))
        return roi_cls_locs, roi_scores

这里需要注意的是参数classifier,回顾下网络的构建源码:

def resnet50():
    model = ResNet(Bottleneck, [3, 4, 6, 3])
    #----------------------------------------------------------------------------#
    #   获取特征提取部分,从conv1到model.layer3,最终获得一个38,38,1024的特征层
    #----------------------------------------------------------------------------#
    features = list([model.conv1, model.bn1, model.relu, model.maxpool, model.layer1, model.layer2, model.layer3])
    #----------------------------------------------------------------------------#
    #   获取分类部分,从model.layer4到model.avgpool
    #----------------------------------------------------------------------------#
    classifier = list([model.layer4, model.avgpool])
    
    features = nn.Sequential(*features)
    classifier = nn.Sequential(*classifier)
    return features, classifier

classifier包含的是resnet50的layer4的全部卷积层,以及一个kernel-size为7的avgpooling。
对于Resnet50RoIHead这个类而言,输入x是backbone的输出,因为这里是每个图片单独来生成batch数据的,那么假设输入图片是800*800,那么x的shape是(1,1024,50,50),rois已经通过正负样本的采样(下节训练部分解释),变成了(128,4)。

        rois_feature_map = torch.zeros_like(rois)
        rois_feature_map[:, [0,2]] = rois[:, [0,2]] / img_size[1] * x.size()[3]
        rois_feature_map[:, [1,3]] = rois[:, [1,3]] / img_size[0] * x.size()[2]

这里把rois的坐标放缩到了feature_map的尺度上方便进行ROI Pooling。

indices_and_rois = torch.cat([roi_indices[:, None], rois_feature_map], dim=1)

这里合并了索引和rois数值。

self.roi = RoIPool((roi_size, roi_size), spatial_scale)
pool = self.roi(x, indices_and_rois)

后面调用了ROI Pooling,将ROI归并到统一的shape大小上,这部分ROI Pooling的实现在源代码中是C实现的,暂时不在本系列的范围内,后面考虑单独做一个课题(针对NMS、ROI Pooling等C/C++实现的代码)。
pool的shape为(128,1024,14,14),可见实现了feature map的裁剪,归并到了统一的大小。

fc7 = self.classifier(pool)

这里的classifier就是前面提到的resnet50的stage4+avgpool(7)。
shape的变换:(128,1024,14,14)=>(128,2048,7,7)=>(128,2048,1,1)
所以最终经过classifier的输出为(128,2048,1,1)。

fc7 = fc7.view(fc7.size(0), -1)

展开为(128,2048)。

  roi_cls_locs = self.cls_loc(fc7)
        roi_scores = self.score(fc7)
        roi_cls_locs = roi_cls_locs.view(n, -1, roi_cls_locs.size(1))
        roi_scores = roi_scores.view(n, -1, roi_scores.size(1))

最终输入两个全连接层,看前面的定义,这两个全连接是:

 self.cls_loc = nn.Linear(2048, n_class * 4)
 self.score = nn.Linear(2048, n_class)

对于分类得分,每个框输出n_class个分数,对于VOC数据集,输出(128,21)。
对于位置回归,每个框输出n_class * 4个数值,对于VOC数据集,输出(128,84)。

以上我们通过debug的方式分析了Classifier网络对ROI进行回归的方式,其中有部分内容比如RPN的输出是如何进行sample的?正负样本是如何确定和划分的?loss是如何计算的?
这些内容我们留到下一篇Faster-RCNN的训练流程去详细分析。

谢谢各位阅读,支持博主请点赞收藏,谢谢!

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值