1、参考来源

(1)VGG的实现参考如下的

Pytorch 搭建自己的SSD目标检测平台(Bubbliiiing 深度学习 教程)_哔哩哔哩_bilibili

博主的源码地址

GitHub - bubbliiiing/ssd-pytorch: 这是一个ssd-pytorch的源码,可以用于训练自己的模型。

(2)另外一个参考是resnet做骨干网实现的 

2.2 SSD源码解析(Pytorch)_哔哩哔哩_bilibili

2、原理

2.1基础

 (1)检测位置的标注

 (2)SSD对图片调整为300*300,划分为6个尺度进行检测

 (3)基础原理

 3、代码实现

3.1第一部分VGG backbone的代码实现

自习对照第一部分代码的分析

(10)SSD第一部分网络的手工实现_chencaw的博客-CSDN博客

 参照了本次博主的代码

VGG-SSD特征提取网络代码详解_哔哩哔哩_bilibili

具体代码为(该代码也可以考虑修改添加normalization): 

import torch.nn as nn
from torch.hub import load_state_dict_from_url


'''
该代码用于获得VGG主干特征提取网络的输出。
输入变量i代表的是输入图片的通道数,通常为3。

300, 300, 3 -> 300, 300, 64 -> 300, 300, 64 -> 150, 150, 64 -> 150, 150, 128 -> 150, 150, 128 -> 75, 75, 128 ->
75, 75, 256 -> 75, 75, 256 -> 75, 75, 256 -> 38, 38, 256 -> 38, 38, 512 -> 38, 38, 512 -> 38, 38, 512 -> 19, 19, 512 ->
19, 19, 512 -> 19, 19, 512 -> 19, 19, 512 -> 19, 19, 512 -> 19, 19, 1024 -> 19, 19, 1024

#总共35层,对应0---34
38, 38, 512的序号是22  ,
Conv4_3的层数序号是22,连接第1个分类器,包含relu层;但是给分类器loc_layers和conf_layers计算的时候
为了函数计算方便不包含relu层,提供了层数21的输出维度,陈修订20221014

19, 19, 1024的序号是34, 
Conv7的层数序号是34,连接第2个分类器,包含relu层;但是给分类器loc_layers和conf_layers计算的时候
为了函数计算方便不包含relu层,提供了层数33的输出维度,陈修订20221014
'''
base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
            512, 512, 512]

def vgg(pretrained = False):
    layers = []
    in_channels = 3
    for v in base:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        elif v == 'C':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    # 19, 19, 512 -> 19, 19, 512 
    pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
    # 19, 19, 512 -> 19, 19, 1024
    conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
    # 19, 19, 1024 -> 19, 19, 1024
    conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
    layers += [pool5, conv6,
               nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]

    model = nn.ModuleList(layers)
    if pretrained:
        state_dict = load_state_dict_from_url("https://download.pytorch.org/models/vgg16-397923af.pth", model_dir="./model_data")
        state_dict = {k.replace('features.', '') : v for k, v in state_dict.items()}
        model.load_state_dict(state_dict, strict = False)
    return model

if __name__ == "__main__":
    net = vgg()
    for i, layer in enumerate(net):
        print(i, layer)

(0)有人说Conv4_3和Conv7连接分类器的时候不包括relu,验证后感觉应该要包含relu的

(1)Conv4_3的层数序号是22,连接第1个分类器,包含relu层;但是给分类器loc_layers和conf_layers计算的时候为了函数计算方便不包含relu层,提供了层数21的输出维度,陈修订20221014

(2)Conv7的层数序号是34,连接第2个分类器,包含relu层;但是给分类器loc_layers和conf_layers计算的时候为了函数计算方便不包含relu层,提供了层数33的输出维度,陈修订20221014

(3)Conv4_3后续还要L2 normalization,陈确认

 3.2 几个额外的卷积层extra网络

(1)网络结构图如下

图1 额外部分网络结构图 

(2)这几层网络的主要内容

参考了

ssd网络结构_目标检测--SSD(附pytorch代码详解)_weixin_39907596的博客-CSDN博客

图2 额外部分网络内容  

 (3)额外层的代码实现方法一(简单,推荐):

def add_extras(in_channels, backbone_name):
    layers = []
    if backbone_name == 'vgg':
        # Block 6
        # 19,19,1024 -> 19,19,256 -> 10,10,512
        layers += [nn.Conv2d(in_channels, 256, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)]

        # Block 7
        # 10,10,512 -> 10,10,128 -> 5,5,256
        layers += [nn.Conv2d(512, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)]

        # Block 8
        # 5,5,256 -> 5,5,128 -> 3,3,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
        
        # Block 9
        # 3,3,256 -> 3,3,128 -> 1,1,256
        layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)]
        layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)]
    else:     #Inverted Residual  倒置残差 窄 -> 宽 -> 窄 应该是配合mobilenetv2的
        layers += [InvertedResidual(in_channels, 512, stride=2, expand_ratio=0.2)]
        layers += [InvertedResidual(512, 256, stride=2, expand_ratio=0.25)]
        layers += [InvertedResidual(256, 256, stride=2, expand_ratio=0.5)]
        layers += [InvertedResidual(256, 64, stride=2, expand_ratio=0.25)]
        
    return nn.ModuleList(layers)

 (4)额外层的代码实现方法二:

待分析

# [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256]
def add_extras(cfg, i, batch_norm=False):
    # Extra layers added to VGG for feature scaling
    layers = []
    in_channels = i
    flag = False  # False means 0, True means 1
    for k, v in enumerate(cfg):
        if in_channels != 'S':
            if v == 'S':
                layers += [nn.Conv2d(in_channels, cfg[k + 1],
                           kernel_size=(1, 3)[flag], stride=2, padding=1)]
            else:
                layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]
            flag = not flag
        in_channels = v
    return layers

 3.3  输出的分类器和第一部分、额外部分的连接

(1)Conv4_3不带relu的输出接第1个classifer;Conv7连接第2个分类器,不包含relu;对的,陈修订20221014。  对应vgg_backbone的第21层和33层,将上面第一个代码

(2)对照图2 额外部分网络内容,额外部分共8个层,对应0--7;它对应分类器的是1,3,5,7层,陈20221014  

代码实现

class SSD300(nn.Module):
    def __init__(self, num_classes, backbone_name, pretrained = False):
        super(SSD300, self).__init__()
        self.num_classes    = num_classes
        if backbone_name    == "vgg":
            self.vgg        = add_vgg(pretrained)
            self.extras     = add_extras(1024, backbone_name)
            self.L2Norm     = L2Norm(512, 20)
            mbox            = [4, 6, 6, 6, 4, 4]
            
            loc_layers      = []
            conf_layers     = []
            backbone_source = [21, -2]
            #---------------------------------------------------#
            #   在add_vgg获得的特征层里
            #   第21层和-2层的维度可以用来进行回归预测和分类预测。
            #   分别是conv4-3(38,38,512)和conv7(19,19,1024)的输出
            #---------------------------------------------------#
            for k, v in enumerate(backbone_source):
                loc_layers  += [nn.Conv2d(self.vgg[v].out_channels, mbox[k] * 4, kernel_size = 3, padding = 1)]
                conf_layers += [nn.Conv2d(self.vgg[v].out_channels, mbox[k] * num_classes, kernel_size = 3, padding = 1)]
            #-------------------------------------------------------------#
            #   在add_extras获得的特征层里
            #   第1层、第3层、第5层、第7层可以用来进行回归预测和分类预测。
            #   shape分别为(10,10,512), (5,5,256), (3,3,256), (1,1,256)
            #-------------------------------------------------------------#  
            #共8个层0--7,对应分类器的是1,3,5,7层,陈20221014
            # enumerate(names, 1),后面的参数1就是本次循环的起始索引,替换默认的0:
            #下面的k从2开始
            for k, v in enumerate(self.extras[1::2], 2):
                # print("chen test k ",k)
                loc_layers  += [nn.Conv2d(v.out_channels, mbox[k] * 4, kernel_size = 3, padding = 1)]
                conf_layers += [nn.Conv2d(v.out_channels, mbox[k] * num_classes, kernel_size = 3, padding = 1)]
        else: #应该是配合mobilenetv2的,不用管
            pass

        self.loc            = nn.ModuleList(loc_layers)
        self.conf           = nn.ModuleList(conf_layers)
        self.backbone_name  = backbone_name

 3.4  前向传播,特征提前的部分代码

具体原理可以参照上一个教程

(11)目标检测_SSD的特征提取和BOX个数和预测原理_chencaw的博客-CSDN博客

对应的代码内容如下:

(1)前向传播的代码如下:

  def forward(self, x):
        print("chen forward1 ")
        #---------------------------#
        #   x是300,300,3
        #---------------------------#
        sources = list()
        loc     = list()
        conf    = list()

        #---------------------------#
        #   获得conv4_3的内容
        #   shape为38,38,512
        #   第一部分0--22层,陈
        #   对应到conv4_3的relu输出,陈
        #---------------------------#
        if self.backbone_name == "vgg":
            for k in range(23):
                x = self.vgg[k](x)
        else:
            for k in range(14):
                x = self.mobilenet[k](x)
        #---------------------------#
        #   conv4_3的内容
        #   需要进行L2标准化
        #---------------------------#
        s = self.L2Norm(x)
        sources.append(s)

        #---------------------------#
        #   获得conv7的内容
        #   shape为19,19,1024
        #   23--34层,包括了conv7后面的relu输出,陈
        #---------------------------#
        if self.backbone_name == "vgg":
            for k in range(23, len(self.vgg)):
                x = self.vgg[k](x)
        else:
            for k in range(14, len(self.mobilenet)):
                x = self.mobilenet[k](x)

        sources.append(x)
        #-------------------------------------------------------------#
        #   在add_extras获得的特征层里
        #   额外部分共8层,对应0--7,陈
        #   第1层、第3层、第5层、第7层可以用来进行回归预测和分类预测。
        #   shape分别为(10,10,512), (5,5,256), (3,3,256), (1,1,256)
        #-------------------------------------------------------------#      
        for k, v in enumerate(self.extras):
            x = F.relu(v(x), inplace=True)
            if self.backbone_name == "vgg":
                if k % 2 == 1:
                    sources.append(x)
            else:
                sources.append(x)

        #-------------------------------------------------------------#
        #   为获得的6个有效特征层添加回归预测和分类预测
        #
        #   陈注释:
        #   结论:这段代码的作用是,通过一个不常见的python迭代器函数zip(),
        #   将6个卷积层的输出sources[],经过6个不同的分类器(每个self.loc和self.conf为一对),
        #   最后将得的回归和分类结果,并且还reshape了一下。

        #   .permute将tensor的维度换位,如图片img的size比如是(28,28,3)就
        #   可以利用img.permute(2,0,1)得到一个size为(3,28,28)的tensor。
        #    陈注释:
        #   .contiguous()配合torch.permute()、torch.transpose()、torch.view()方法一起使用
        #   使得转换后的维度连续可用
        #   zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
        #    zip([iterable, ...])    iterable -- 一个或多个迭代器;
        #-------------------------------------------------------------#
        # 其中 sources为list  ,self.loc 和self.conf为nn.ModuleList
        # 局部变量loc好conf为list
        print("List 类型sources的大小:",len(sources))
        for i in range(len(sources)):
            print("sources[]",i,sources[i].shape)

        print("查看 self.loc内容")
        print(self.loc) 
        print("查看 self.conf")
        print(self.conf) 


        chen_count =0
        # 分类器1输出为例子,class_num =3
        # 下述代码是将 l(x)维度未处理的shape : (1, 16, 38, 38)转为l(x)维度处理后的shape : (1, 38, 38, 16)   
        # c(x)维度未处理的shape : (1, 12, 38, 38) ,c(x)维度处理后的shape : (1, 38, 38, 12)
        for (x, l, c) in zip(sources, self.loc, self.conf):
            chen_count += 1
            print(chen_count, "输入x的shape:", x.shape)

            ltest_init = l(x).detach().numpy()
            ltest = l(x).permute(0, 2, 3, 1).contiguous().detach().numpy()
            print("l(x)维度未处理的shape :", ltest_init.shape) 
            print("l(x)维度处理后的shape :", ltest.shape) 

            ctest_init = c(x).detach().numpy()
            ctest = c(x).permute(0, 2, 3, 1).contiguous().detach().numpy()
            print("c(x)维度未处理的shape :", ctest_init.shape) 
            print("c(x)维度处理后的shape :", ctest.shape) 

            loc.append(l(x).permute(0, 2, 3, 1).contiguous())
            conf.append(c(x).permute(0, 2, 3, 1).contiguous())
            
        #-------------------------------------------------------------#
        #   陈:下面代码的作用是先将分类回归结果合并堆起来,从LIST中堆起来否则不好处理!
        #       因为loc和conf是list结构
        #
        #   进行reshape方便堆叠 loc[]直接转换为一维 torch.Size([34928])
        #                      conf[] 直接转换为一维 torch.Size([26196])
        #-------------------------------------------------------------#  
        print("List 类型loc和conf的原始大小:", len(loc), len(loc))
        for i in range(len(loc)):
            print(i,"loc[]",loc[i].shape)
            print(i,"conf[]",conf[i].shape)

        loc     = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
        conf    = torch.cat([o.view(o.size(0), -1) for o in conf], 1)
        
        print("处理后loc和conf的shape:")
        for i in range(len(loc)):
            print(i,"loc[]",loc[i].shape)
            print(i,"conf[]",conf[i].shape)


        #-------------------------------------------------------------#
        #   下面的代码通过元组,给了一个漂亮的输出
        #   loc会reshape到batch_size, num_anchors, 4
        #   conf会reshap到batch_size, num_anchors, self.num_classes
        #-------------------------------------------------------------#      
        output = (
            loc.view(loc.size(0), -1, 4),
            conf.view(conf.size(0), -1, self.num_classes),
        )
        print("最后的输出output")
        print(output[0].shape,output[1].shape)
        return output

(2)具体for (x, l, c) in zip(sources, self.loc, self.conf):的含义见教程(13) ,主要功能是:

:这段代码的作用是,通过一个不常见的python迭代器函数zip(),将6个卷积层的输出sources[],经过6个不同的分类器(每个self.loc和self.conf为一对),最后将得的回归和分类结果,并且还reshape了一下。

# 分类器1输出为例子,class_num =3
# 下述代码是将 l(x)维度未处理的shape : (1, 16, 38, 38)转为l(x)维度处理后的shape : (1, 38, 38, 16)   
# c(x)维度未处理的shape : (1, 12, 38, 38) ,c(x)维度处理后的shape : (1, 38, 38, 12)

(13)目标检测_SSD算法中几个奇怪函数的研究_chencaw的博客-CSDN博客

(3)代码torch.cat的含义是先将结果堆起来

陈:下面代码的作用是先将分类回归结果合并堆起来,从LIST中堆起来否则不好处理!

因为loc和conf是list结构

        loc     = torch.cat([o.view(o.size(0), -1) for o in loc], 1)
        conf    = torch.cat([o.view(o.size(0), -1) for o in conf], 1)

测试结果是

(4)前向网络最后的输出

#   下面的代码通过元组,给了一个漂亮的输出

        #-------------------------------------------------------------#
        #   loc会reshape到batch_size, num_anchors, 4
        #   conf会reshap到batch_size, num_anchors, self.num_classes
        #-------------------------------------------------------------#      
        output = (
            loc.view(loc.size(0), -1, 4),
            conf.view(conf.size(0), -1, self.num_classes),
        )
        print("最后的输出output")
        print(output[0].shape,output[1].shape)
        return output

对应输出

1)  loc会reshape到batch_size, num_anchors, 4

2) conf会reshap到batch_size, num_anchors, self.num_classes 

更多推荐