让我们来了解一下SSD。
SD方法基于一个前馈卷积网络,该网络生成一个固定大小的边界框集合和每个框的对象类别分数,避免了传统目标检测方法中边界框建议和重采样的阶段。这种方法显著提高了检测速度。此外,SSD通过在不同层次的特征图上进行检测,允许模型利用不同尺度的信息,从而提高对各种尺寸和形状目标的检测性能。
主要特点:
- 高效的检测方法:SSD是一种用于多类别检测的单次检测器,相比之前的单次检测器(如YOLO)更快,准确度也明显提高,与那些执行显式区域建议和池化的较慢技术(包括Faster R-CNN)几乎一样准确。
- 创新的框架设计:SSD的核心是应用于特征图的小型卷积滤波器,用于预测一组固定默认边界框的类别分数和框偏移量。
- 多尺度特征图与硬负挖掘:SSD利用多尺度特征图和硬负挖掘技术,改进了检测精度,尤其是对于不同尺寸和形状的对象。
技术原理:
SSD框架:
SSD在训练时只需要输入图像和每个对象的真实边框。按照卷积的方式,在几个具有不同尺度(例如 8×8 和 4×4 在 (b) 和(c)的特征图每个位置,我们评估一小组(例如 4 个)不同宽高比的默认边框。对于每一个默认边框,我们预测所有对象类别的形状偏移和置信度((c1,c2,…))。在训练时,我们首先将这些默认边框与真实边框进行匹配。例如,我们将两个默认边框分别与猫和狗匹配,它们被视为正样本,其余的则视为负样本。模型损失是定位损失和置信度损失的加权和。
网络会使用多个特征层来进行检测任务,每个特征层可能是网络新添加的,或者是从网络的基础部分(如VGG或ResNet等)继承的现有层。每个特征层都会通过一系列的卷积滤波器来生成检测预测,而这些预测再通过网络的顶部结构输出(如图2所示)。
具体来说,对于特征层的每一个单元区域(比如大小为m×n),网络会使用小的3×3×p的卷积核来预测目标的类别分数或相对于默认框坐标的位置偏移。应用该卷积核于特征层的每一个位置上,会输出一个预测值。对边界框的位置预测是相对于特征层上对应位置的默认框来测量的。
默认框是预先设定的一组形状和大小,它们对应于不同特征图单元的位置。在每个特征图单元,网络会预测相对于该单元中默认框形状的偏移量,以及每个框可能包含类别实例的置信度(类分数)。简单来说,对于每个默认框,网络会计算每个类别的置信度以及4个与默认框形状的位置偏移值。对于m×n大小的特征层,总共就会有(c+4)kmn个输出。
SSD中的默认框在概念上与Faster R-CNN中使用的锚点框类似,但SSD将这些默认框应用到多个分辨率的特征图上,这样可以有效地覆盖不同尺寸和比例的目标,提高检测的准确性和灵活性。
让我们来实现一下这个模型:
首先是SSD网络:
class L2Norm(nn.Module):
def __init__(self,n_channels, scale):
super(L2Norm,self).__init__()
self.n_channels = n_channels
self.gamma = scale or None
self.eps = 1e-10
self.weight = nn.Parameter(torch.Tensor(self.n_channels))
self.reset_parameters()
def reset_parameters(self):
init.constant_(self.weight,self.gamma)
def forward(self, x):
norm = x.pow(2).sum(dim=1, keepdim=True).sqrt()+self.eps
x = torch.div(x,norm)
out = self.weight.unsqueeze(0).unsqueeze(2).unsqueeze(3).expand_as(x) * x
return out
def add_extras(in_channels, backbone_name):
layers = []
if backbone_name == '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)]
else:
# 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)]
return nn.ModuleList(layers)
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(p