2.3、SSD的训练过程与细节
2.3.1、框架训练的具体步骤
- 首先VGG16在ILSVRC CLS-LOC数据集上进行预训练。
- 然后将VGG16的全连接层fc6和fc7转换成3×3卷积层Conv6(Conv6采用带孔卷积Dilation Convolution,Conv6采用3×3大小,dilation rate=6的膨胀卷积)和1×1卷积层Conv7;
- 然后移除dropout层和fc8层,并新增一系列卷积层,在检测数据集上做fine tuning;
- 从后面新增的卷积层中提取Conv7,Conv8_2,Conv9_2,Conv10_2,Conv11_2作为检测所用的特征图,加上Conv4_3层,共提取了6个特征图,其大小分别(38,38),(19,19),(10,10),(5,5),(3,3),(1,1),但是不同特征图设置的先验框数目不同(同一个特征图上每个单元设置的先验框是相同的,这里的数目指的是一个单元的先验框数目),由于每个先验框都会预测一个边界框,所以 SSD300一共可以预测38×38×4+19×19×6+10×10×6+5×5×6+3×3×4+1×1×4 = 8732 个边界框,这是一个相当大的数字, 所以说SSD本质上是密集采样;
- 得到了特征图之后,对特征图进行3x3卷积得到检测结果;
- 对于每个预测框,首先根据类别置信度确定其类别(置信度最大者)与置信度值, 并过滤掉属于背景的预测框。
- 然后根据置信度阈值(如0.5)过滤掉阈值较低的预测框。对于留下的预测框进行解码,根据先验框得 到其真实的位置参数(解码后一般还需要做clip,防止预测框位置超出图片)。解码之后,一般需要 根据置信度进行降序排列,然后仅保留top-k(如400)个预测框。
- 使用非最大值抑制NMS进行筛选,过滤掉那些重叠度较大的预测框。最后剩余的预测框就是检测结果。
base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512] def vgg(i): layers = [] in_channels = i 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 pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1) conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6) conv7 = nn.Conv2d(1024, 1024, kernel_size=1) layers += [pool5, conv6, nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)] return layers def add_extras(i, batch_norm=False): # Extra layers added to VGG for feature scaling layers = [] in_channels = i # Block 6 # 19,19,1024 -> 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 -> 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 -> 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 -> 1,1,256 layers += [nn.Conv2d(256, 128, kernel_size=1, stride=1)] layers += [nn.Conv2d(128, 256, kernel_size=3, stride=1)] return layers
2.3.2、特征图的检测过程:
检测值包含两个部分:类别置信度和边界框位置, 各采用一次3×3卷积来进行完成。
每一个有效特征层对应的先验框对应着该特征层上 每一个网格点上 预先设定好的多个框。所有的特征层对应的预测结果的shape如下:
class SSD(nn.Module): def __init__(self, phase, base, extras, head, num_classes): super(SSD, self).__init__() self.phase = phase self.num_classes = num_classes self.cfg = Config self.vgg = nn.ModuleList(base) self.L2Norm = L2Norm(512, 20) self.extras = nn.ModuleList(extras) self.priorbox = PriorBox(self.cfg) with torch.no_grad(): self.priors = Variable(self.priorbox.forward()) self.loc = nn.ModuleList(head[0]) self.conf = nn.ModuleList(head[1]) if phase == 'test': self.softmax = nn.Softmax(dim=-1) self.detect = Detect(num_classes, 0, 200, 0.01, 0.45) def forward(self, x): sources = list() loc = list() conf = list() # 获得conv4_3的内容 for k in range(23): x = self.vgg[k](x) s = self.L2Norm(x) sources.append(s) # 获得fc7的内容 for k in range(23, len(self.vgg)): x = self.vgg[k](x) sources.append(x) # 获得后面的内容 for k, v in enumerate(self.extras): x = F.relu(v(x), inplace=True) if k % 2 == 1: sources.append(x) # 添加回归层和分类层 for (x, l, c) in zip(sources, self.loc, self.conf): loc.append(l(x).permute(0, 2, 3, 1).contiguous()) conf.append(c(x).permute(0, 2, 3, 1).contiguous()) # 进行resize 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) if self.phase == "test": # loc会resize到batch_size,num_anchors,4 # conf会resize到batch_size,num_anchors, output = self.detect( loc.view(loc.size(0), -1, 4), # loc preds self.softmax(conf.view(conf.size(0), -1, self.num_classes)), # conf preds self.priors ) else: output = ( loc.view(loc.size(0), -1, 4), conf.view(conf.size(0), -1, self.num_classes), self.priors ) return output mbox = [4, 6, 6, 6, 4, 4] def get_ssd(phase,num_classes): vgg, extra_layers = add_vgg(3), add_extras(1024) loc_layers = [] conf_layers = [] vgg_source = [21, -2] for k, v in enumerate(vgg_source): loc_layers += [nn.Conv2d(vgg[v].out_channels, mbox[k] * 4, kernel_size=3, padding=1)] conf_layers += [nn.Conv2d(vgg[v].out_channels, mbox[k] * num_classes, kernel_size=3, padding=1)] for k, v in enumerate(extra_layers[1::2], 2): 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)] SSD_MODEL = SSD(phase, vgg, extra_layers, (loc_layers, conf_layers), num_classes) return SSD_MODEL
2.3.3、Anchor中心的获取
每个默认框的中心位置为:
其中,W、H分别输入图像宽度和高度,i=0,1,...,m-1,j=0,1,...,n-1。
2.3.4、数据增强
为了提高关于目标大小和形状的鲁棒性,SSD采用了数据增强的随机采样策略。
- 使用整幅原始输入图像
- 采样图像块,与目标的最小交并比为0.1、0.3、0.5、0.7、0.9
- 随机采样图像块,每个采样块的大小为原始图像大小的[0.1,1],高宽比为[0.5,2],如果真是边框的中心在采样块内,则保留重叠部分;
- 采样块的增强(亮度变化、大小调整、概率翻转等操作)。
2.4、SSD的优缺点
2.4.1、SSD的优点
- 对于小尺寸目标对象,SSD的性能比Faster R-CNN差。SSD只能在较高分辨率的层(最左边的层)检测小 目标。但是这些层包含低级特征,如边缘或色块,分类的信息量较少。
- 准确率随着默认边界框的数量而增加,但以速度为代价。
- 多尺度特征图改进了不同尺度的目标的检测。
- 设计更好的默认边界框将有助于准确性。
- COCO数据集具有较小的目标。要提高准确性,使用较小的默认框(以较小的尺度0.15开始)。
- 与R-CNN相比,SSD具有较低的定位误差,但处理相似类别的分类错误较多。较高的分类错误可能是因为使 用相同的边界框来进行多个类别预测。
- SSD512 具有比SSD300更高的精度(2.5%),但运行速度为22 FPS而不是59 FPS。
2.4.2、SSD的缺点
- SSD算法对小目标不够鲁棒(会出现误检和漏检);
- 最主要的原因是浅层特征图的表示能力 不够强。
3、SSD的改进——DSSD
3.1、DSSD模型概览
DSSD在原来的SSD模型上主要作了两大改进:
- 替换掉VGG,而改用了Resnet-101作为特征提取网络并在对不同尺度的特征图进行默认框检测时使用了更新的检测单元;
- 在网络的后端使用了多个反卷积层(deconvolution layers)以有效地扩展低维度信息 的上下文信息(contextual information),从而有效地改善了小尺度目标的检测。
3.2、新的预测模块
为每个预测层添加一个残差块,如(c)所示。
还尝试了原始的SSD方法(a)和具有跳跃连接(b)的残差块的版本以及两个连续的残差块(d)。
3.3、反卷积模块
DSSD的第二个重大创新来自于在模型后端添加了多个反卷积模块来扩大模型在小尺度上的high level特征信息。而这 种反卷积输出的特征图又会与模型前端卷积层的相同尺度的特征图进行元素级的乘法
(element-wise product)来 生成相应尺度的特征图。
添加额外的反卷积层以连续增加特征图层的分辨率。
为了加强特征,采用了沙漏模型中的“跳跃连接”理念
- 首先,在每个卷积层之后添加BN层。
- 其次,使用学习的反卷积层而不是双线性上采样。
- 最后,测试了不同的 组合方法:逐元素相加和逐元素乘积。
实验结果表明,逐元素乘积提供了最佳的准确性。新的DSSD模型能够胜过以前的SSD框架,特别是在小目标或上下文特定目标上, 同时仍然保持与其他检测器相当的速度。