以resnet50为backbone的ssd模型以及代码总结

ssd模型图示

模型原理

ssd主要的思想是以cnn做为特征提取网络,例如以resnet50做为提取网络,删除掉resnet后面的全连接层,再增添几层额外的卷基层提取特征,得到不同尺度的特征图,然后我们让这些不同层次的特征图分别预测不同大小的目标,浅层卷积层提取到的是比较细小的特征,越深层的卷积提取到的信息会越丰富,因此我们让浅层的卷积特征图去检测小的目标,让深层的卷积特征图去检测大的目标。
还是直接上图和代码吧
在这里插入图片描述

ssd原论文应该是使用的vgg做为backbone,这里我做了一点修改,使用更优秀的resnet50做为backbone提取原图的特征信息。
首先,ssd接收的输入图像是300300的彩色rgb图像,即原始输入是input_size (3,300,3),依次经过以下卷积层
具体流程见我在草稿纸上推的流程,需要注意的一点是ssd的resnet50与普通resnet50有一点不同就是他在resnet的conv4_的第一个block中把s从2设置成了1,目的是不让通过conv4之后特征图尺寸缩小一半,因此可以看到通过conv3_的特征图尺寸为512
3838,通过conv4_之后尺寸为10243838,并没有缩减一半到1919.

在这里插入图片描述
由上图推导可以看出我们再resnet之后增添了5个层再加上resnet的conv4_得到的特征图,最终得到了6个特征图分别是
feature_map1(1024, 38, 38)
feature_map2(512, 19, 19)
feature_map3(512, 10, 10)
feature_map4(256, 5, 5)
feature_map5(256, 3, 3)
feature_map6(256, 1, 1)

backbone代码

class Backbone(nn.Module):
    def __init__(self, pretrain_path=None):
        super(Backbone, self).__init__()
        net = resnet50()
        # outchannels指输出的六个features_map的维度
        self.out_channels = [1024, 512, 512, 256, 256, 256]

        if pretrain_path is not None:
            net.load_state_dict(torch.load(pretrain_path))
		# 为网络定义特征提取器,是resnet从第一个卷积层到conv4_
        self.feature_extractor = nn.Sequential(*list(net.children())[:7])
		
        conv4_block1 = self.feature_extractor[-1][0]

        # 修改conv4_block1的步距,从2->1,不让特征图尺寸缩减一半
        conv4_block1.conv1.stride = (1, 1)
        conv4_block1.conv2.stride = (1, 1)
        conv4_block1.downsample[0].stride = (1, 1)

    def forward(self, x):  # 得到第一个feature_map1
        x = self.feature_extractor(x)
        return x

下面是为ssd增添额外卷积层的代码

    def _build_additional_features(self, input_size):
        """
        为backbone(resnet50)添加额外的一系列卷积层,得到相应的一系列特征提取
        """
        additional_blocks = []  # 存放额外卷积层的列表
        # input_size = [1024, 512, 512, 256, 256, 256] for resnet50
        middle_channels = [256, 256, 128, 128, 128]
        # input_size[:-1]=[1024, 512, 512, 256, 256], input_size[1:]=[512, 512, 256, 256, 256]
        for i, (input_ch, output_ch, middle_ch) in enumerate(zip(input_size[:-1], input_size[1:], middle_channels)):
            padding, stride = (1, 2) if i < 3 else (0, 1)
            layer = nn.Sequential(
                nn.Conv2d(input_ch, middle_ch, kernel_size=1, bias=False),
                nn.BatchNorm2d(middle_ch),
                nn.ReLU(inplace=True),
                nn.Conv2d(middle_ch, output_ch, kernel_size=3, padding=padding, stride=stride, bias=False),
                nn.BatchNorm2d(output_ch),
                nn.ReLU(inplace=True),
            )
            additional_blocks.append(layer)
        self.additional_blocks = nn.ModuleList(additional_blocks)

ssd模型代码

class SSD300(nn.Module):
    def __init__(self, backbone=None, num_classes=21):
        super(SSD300, self).__init__()
        self.feature_extractor = backbone  # 把传入的backbone定义给ssd的feature_extractor
        self.num_classes = num_classes
        # out_channels = [1024, 512, 512, 256, 256, 256] for resnet50,添加一系列卷积层做为特征提取层
        self._build_additional_features(self.feature_extractor.out_channels)
        # 第一个特征层层和最后两个特征层默认每个点生成四个大小的框, 其他层每个点默认生成6个
        self.num_defaults = [4, 6, 6, 6, 4, 4]
        location_extractors = []  # 定位预测器,使用3*3卷积来预测定位
        confidence_extractors = []  # 置信度预测器, 同样使用3*3卷积来预测

        # out_channels = [1024, 512, 512, 256, 256, 256] for resnet50
        for nd, oc in zip(self.num_defaults, self.feature_extractor.out_channels):
            # nd is number_default_boxes, oc is output_channel
            location_extractors.append(nn.Conv2d(oc, nd * 4, kernel_size=3, padding=1))
            confidence_extractors.append(nn.Conv2d(oc, nd * self.num_classes, kernel_size=3, padding=1))

        self.loc = nn.ModuleList(location_extractors)
        self.conf = nn.ModuleList(confidence_extractors)
        self._init_weights()

        # default_box.dboxes  (8732*4)
        default_box = dboxes300_coco()  # 通过该函数生成默认的8732个default box
        self.compute_loss = Loss(default_box)
        self.encoder = Encoder(default_box)
        self.postprocess = PostProcess(default_box)

ssd模型前向传播过程

    def forward(self, image, targets=None):
        x = self.feature_extractor(image)

        # Feature Map 38x38x1024, 19x19x512, 10x10x512, 5x5x256, 3x3x256, 1x1x256
        detection_features = torch.jit.annotate(List[Tensor], [])  # [x]
        detection_features.append(x)
        # 遍历额外增加层使得通过resnet之后的x分别经过这些额外卷积层得到一系列feature_map然后把6个feature_map都放进detection_features列表中
        for layer in self.additional_blocks:
            x = layer(x)
            detection_features.append(x)

        # Feature Map 38x38x4, 19x19x6, 10x10x6, 5x5x6, 3x3x4, 1x1x4
        # locs:(batch_size, 4, 8732), confs:(batch_size, 21, 8732)
        locs, confs = self.bbox_view(detection_features, self.loc, self.conf)

        # For SSD 300, shall return nbatch x 8732 x {nlabels, nlocs} results
        # 38x38x4 + 19x19x6 + 10x10x6 + 5x5x6 + 3x3x4 + 1x1x4 = 8732

        if self.training:
            if targets is None:
                raise ValueError("In training mode, targets should be passed")
            # bboxes_out (Tensor 8732 x 4), labels_out (Tensor 8732)
            bboxes_out = targets['boxes']
            bboxes_out = bboxes_out.transpose(1, 2).contiguous()  # batch_size*4*8732
            # print(bboxes_out.is_contiguous())
            labels_out = targets['labels']  # batch_size*8732
            # print(labels_out.is_contiguous())

            # ploc, plabel, gloc, glabel
            loss = self.compute_loss(locs, confs, bboxes_out, labels_out)
            return {"total_losses": loss}

        # 将预测回归参数叠加到default box上得到最终预测box,并执行非极大值抑制虑除重叠框
        # results = self.encoder.decode_batch(locs, confs)
        results = self.postprocess(locs, confs)
        return results
    def bbox_view(self, features, loc_extractor, conf_extractor):
        locs = []
        confs = []
        for f, l, c in zip(features, loc_extractor, conf_extractor):
            # [batch, n*4, feat_size, feat_size] -> [batch, 4, -1]
            locs.append(l(f).view(f.size(0), 4, -1))  # size([batch_size, 4, -1])
            # [batch, n*classes, feat_size, feat_size] -> [batch, classes, -1]
            confs.append(c(f).view(f.size(0), self.num_classes, -1))  # size([batch_size, num_classes, -1])

        # locs:(batch_size, 4, 8732), confs:(batch_size, 21, 8732)
        locs, confs = torch.cat(locs, 2).contiguous(), torch.cat(confs, 2).contiguous()
        return locs, confs

接下来对重点代码分析以下

def dboxes300_coco():
    figsize = 300  # 输入网络的图像大小
    feat_size = [38, 19, 10, 5, 3, 1]   # 每个预测层的feature map尺寸
    steps = [8, 16, 32, 64, 100, 300]   # 每个特征层上的一个cell在原图上的跨度
    scales = [21, 45, 99, 153, 207, 261, 315]  # 每个特征层上预测的default box的scale
    aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]  # 每个预测特征层上预测的default box的ratios
    dboxes = DefaultBoxes(figsize, feat_size, steps, scales, aspect_ratios)
    return dboxes

算了直接上草稿纸吧。。
生成default_boxes的过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
测试阶段最后得到了bboxes_out(输出格式为边界框左上坐标和右下坐标)和score_out,以及label_out遍历得到每个边框对应的score和label然后进行绘图。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值