(12)目标检测_SSD主干网络基于pytorch搭建代码
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
更多推荐



所有评论(0)