前言
在上一篇中我们详细的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的训练流程去详细分析。
谢谢各位阅读,支持博主请点赞收藏,谢谢!