当前位置:   article > 正文

单阶段多层检测器:SSD (理论及Pytorch代码详解)

单阶段多层检测器:SSD (理论及Pytorch代码详解)

目录

背景:

SSD结构及特点介绍:

数据增强:

基础的VGG结构:

深度卷积层:

先验框与边框特征提取网络

总体网络的前向计算过程

损失函数计算与先验框匹配:

预选框与真实框的匹配

定位损失计算

难样本挖掘

类别损失计算

SSD总结(优缺点简述)


背景:

对于物体检测任务,前面Faster RCNN算法采用了两阶的检测架构,即首先利用RPN网络进行感兴趣区域生成,然后再对该区域进行类别的分类与位置的回归,这种方法虽然显著提升了精度,但也限制了检测速度。YOLO算法利用回归的思想,使用一阶网络直接完成了物体检测,速度很快,但是精度有了明显的下降。 ​ 在此背景下,SSD(Single Shot Multibox Detecor)算法借鉴了Faster RCNN与YOLO的思想,在一阶网络的基础上使用了固定框进行区域生成,并利用了多层的特征信息,在速度与检测精度上都有了一定的提升。 ​ 下面首先介绍SSD方法的主要思想,然后针对重要的结构模块,从代码层面一一解读其实现方法,最后将分析SSD的优缺点,并介绍一些经典的改进算法。、

SSD结构及特点介绍:

img

SSD算法的算法流程如图所示,输入图像首先经过了VGGNet的基础网络,在此之上又增加了几个卷积层,然后利用3×3的卷积核在6个大小与深浅不同的特征层上进行预测,得到预选框的分类与回归预测值,最后直接预测出结果,或者求得网络损失。 ​ SSD的算法思想,主要可以分为4个方面:

  • 数据增强:SSD在数据部分做了充分的数据增强工作,包括光学变换与几何变换等,极大限度地扩充了数据集的丰富性,从而有效提升了模型的检测精度。

  • 网络骨架:SSD在原始VGGNet的基础上,进一步延伸了4个卷积模块,最深处的特征图大小为1×1,这些特征图具有不同的尺度与感受野,可以负责检测不同尺度的物体。

  • PriorBox与多层特征图:与Faster RCNN类似,SSD利用了固定大小与宽高的PriorBox作为区域生成,但与Faster RCNN不同的是,SSD不是只在一个特征图上设定预选框,而是在6个不同尺度上都设立预选框,并且在浅层特征图上设立较小的PriorBox来负责检测小物体,在深层特征图上设立较大的PriorBox来负责检测大物体。原因的是浅层特征层的感受野小,分辨率高,深层特征层感受野大,分辨率低

  • 正、负样本的选取与损失计算:利用3×3的卷积在6个特征图上进行特征的提取,并分为分类与回归两个分支,代表所有预选框的预测值,随后进行预选框与真实框的匹配,利用IoU筛选出正样本与负样本,最终计算出分类损失与回归损失。

由整个过程可以看出,SSD只进行了一次框的预测与损失计算,属于一阶网络。由于利用了多个特征图,SSD实现了较好的检测精度。接下来分点介绍几个部分。

数据增强:

SSD做了丰富的数据增强策略,这部分为模型的mAP带来了8.8%的提升,尤其是对于小物体和遮挡物体等难点,数据增强起到了非常重要的作用。

img

SSD的数据增强 整体流程如图所示,总体上包括光学变换与几何变换两个过程。光学变换包括亮度和对比度等随机调整,可以调整图像像素值的大小,并不会改变图像尺寸; 几何变换包括扩展、裁剪和镜像等操作,主要负责进行尺度上的变化,最后再进行去均值操作。大部分操作都是随机的过程,尽可能保证数据的丰富性。

数据增强的总体流程代码如下:

  1. class SSDAugmentation(object):
  2.  def __init__(self, size=300, mean=(104, 117, 123)):
  3.    self.mean = mean
  4.    self.size = size
  5.    self.augment = Compose([
  6.      \# 首先将图像像素值从整型变成浮点型
  7.      ConvertFromInts(),
  8.      \# 将标签中的边框从比例坐标变换为真实坐标
  9.      ToAbsoluteCoords(),
  10.      \# 进行亮度、对比度、色相与饱和度的随机调整,然后随机调换通道
  11.      PhotometricDistort(),
  12.      Expand(self.mean),     # 随机扩展图像大小,图像仅靠右下方
  13.      RandomSampleCrop(),         # 随机裁剪图像
  14.      RandomMirror(),       # 随机左右镜像
  15.      ToPercentCoords(),     # 从真实坐标变回比例坐标
  16.      Resize(self.size),         # 缩放到固定的300×300大小
  17.      SubtractMeans(self.mean)  # 最后进行去均值
  18.   ])

将像素值从整型变到浮点型,边框从比例坐标变换到真实坐标,这两种变换较为简单,这里不展开叙述。接下来重点介绍光学变换与几何变换这两个重要的变换方法。

  1. 光学变换

    首先是进行亮度调整,具体方法是以0.5的概率为图像中的每一个点加一个实数,该实数随机选取于[-32,32)区间中。具体可见如下RandomBrightness类的实现。

    1. class RandomBrightness(object):
    2.  def __init__(self, delta=32):
    3.    self.delta = delta
    4.  def __call__(self, image, boxes=None, labels=None):
    5.    if random.randint(2):
    6.      \# 随机选取一个位于[-32, 32)区间的数,相加到图像上
    7.      delta = random.uniform(-self.delta, self.delta)
    8.      image += delta
    9.    return image, boxes, labels

    接下来是对比度、色相与饱和度的随机调整。色相的随机调整与亮度很类似,都是随机地加一个数,而对比度与饱和度则是随机乘一个数。另外,对色相与饱和度的调整是在HSV色域空间进行的。由于此三者与亮度很相似,代码不再单独给出。 ​ 关于以上三者的调整顺序,SSD也给了一个随机处理,即有一半的概率对比度在另外两者之前,另一半概率则是对比度在另外两者之后。 光学变换的最后一项是添加随机的光照噪声,具体做法是随机交换RGB三个通道的值,具体实现如RandomLightingNoise()类所示。

    1. class RandomLightingNoise(object):
    2.  def __init__(self):
    3.    self.perms = ((0, 1, 2), (0, 2, 1),
    4.           (1, 0, 2), (1, 2, 0),
    5.           (2, 0, 1), (2, 1, 0))
    6.  def __call__(self, image, boxes=None, labels=None):
    7.    if random.randint(2):
    8.      \# 随机选取一个通道的交换顺序,交换图像三个通道的值
    9.      swap = self.perms[random.randint(len(self.perms))]
    10.      shuffle = SwapChannels(swap)        # shuffle channels
    11.      image = shuffle(image)
    12.    return image, boxes, labels

  2. 几何变换

    在几何变换中,首先进行的是尺度的随机扩展。扩展的具体过程是随机选择一个在[1,4)区间的数作为扩展比例,将原图像放在扩展后图像的右下角,其他区域填入每个通道的均值,即[104,117,123],如图所示。

    代码实现时,首先利用random函数保证有一半的概率进行扩展,然后随机选择一个位于[1,4)区间的比例值,新建一个图像,将均值与原图像素值依次赋予新图,最后再将对应的边框也进行平移,即完成了扩展流程。

    1. class Expand(object):
    2.  def __init__(self, mean):
    3.    self.mean = mean
    4.  def __call__(self, image, boxes, labels):
    5.    if random.randint(2):
    6.      return image, boxes, labels
    7.    \# 求取原图像在新图像中的左上角坐标值
    8.    height, width, depth = image.shape
    9.    ratio = random.uniform(1, 4)
    10.    left = random.uniform(0, width*ratio - width)
    11.    top = random.uniform(0, height*ratio - height)
    12.    \# 建立新的图像,并依次赋值
    13.    expand_image = np.zeros(
    14.     (int(height*ratio), int(width*ratio), depth),
    15.      dtype=image.dtype)
    16.    expand_image[:, :, :] = self.mean
    17.    expand_image[int(top):int(top + height),
    18.           int(left):int(left + width)] = image
    19.    image = expand_image
    20.    \# 对边框也进行相应变换
    21.    boxes = boxes.copy()
    22.    boxes[:, :2] += (int(left), int(top))
    23.    boxes[:, 2:] += (int(left), int(top))
    24.    return image, boxes, labels

    随机扩展后,紧接着进行随机裁剪。SSD的裁剪策略是从图像中随机裁剪出一块,需要保证该图像块至少与一个物体边框有重叠,重叠的比例从{0.1、0.3、0.7、0.9}中随机选取,同时至少有一个物体的中心点落在该图像块中。 这种随机裁剪的好处在于,首先每一个图形块都要有物体,可以过滤掉不包含明显真实物体的图像;同时,不同的重叠比例也极大地丰富了训练集,尤其是针对物体遮挡的情况。由于处理步骤较多,在此不再针对裁剪进行代码说明。 裁剪后进行的是随机的图像镜像,通常是图像的左右翻转,这是一种简单又极为实用的图像增强手段,在多个物体检测算法中都会用到。

    1. class RandomMirror(object):
    2.  def __call__(self, image, boxes, classes):
    3.    _, width, _ = image.shape
    4.    if random.randint(2):
    5.      \# 这里的::代表反向,即将每一行的数据反向遍历,完成镜像
    6.      image = image[:, ::-1]
    7.      boxes = boxes.copy()
    8.      boxes[:, 0::2] = width - boxes[:, 2::-2]
    9.    return image, boxes, classes

    在完成镜像后,最后一步几何变换是固定缩放,默认使用了300×300的输入大小。这里300×300的固定输入大小是经过精心设计的,可以恰好满足后续特征图的检测尺度,例如最后一层的特征图大小为1×1,负责检测的尺度则为0.9。原论文作者也给出了另外一个更精确的500×500输入的版本。 经过上述光学与几何两个变换后,最后一步是常见的去均值处理,具体操作是减去每个通道的均值,具体代码如下:

    1. class SubtractMeans(object):
    2.  def __init__(self, mean):
    3.    self.mean = np.array(mean, dtype=np.float32)
    4.  def __call__(self, image, boxes=None, labels=None):
    5.    image = image.astype(np.float32)
    6.    image -= self.mean
    7.    return image.astype(np.float32), boxes, labels

基础的VGG结构:

img

SSD采用了VGG 16作为基础网络,并在之上进行了一些改善,如图所示。输入图像经过预处理后大小固定为300×300,首先经过VGG 16网络的前13个卷积层,然后利用两个卷积Conv 6与Conv 7取代了原来的全连接网络,进一步提取特征。

针对SSD的基础网络,有以下两点需要注意:

  • 原始的VGG 16的池化层统一大小为2×2,步长为2,而在SSD中,Conv 5后接的Maxpooling层池化大小为3,步长为1,这样做可以在增加感受野的同时,维持特征图的尺寸不变。

  • Conv 6中使用了空洞数为6的空洞卷积,其padding也为6,这样做同样也是为了增加感受野的同时保持参数量与特征图尺寸的不变。 利用PyTorch构造该基础网络时,只需要在官方VGG 16的基础上进行一些修改即可。

SSD的基础网络代码如下:

  1. # 搭建VGG基础网络的函数
  2. def vgg(cfg, i, batch_norm=False):
  3.  layers = []
  4.  in_channels = i
  5.  for v in cfg:
  6.    if v == 'M':
  7.      layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
  8.    elif v == 'C':
  9.      layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
  10.    else:
  11.      conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
  12.      if batch_norm:
  13.        layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
  14.      else:
  15.        layers += [conv2d, nn.ReLU(inplace=True)]
  16.      in_channels = v
  17.  pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
  18.  conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
  19.  conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
  20.  layers += [pool5, conv6,
  21.        nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
  22.  return layers

注意:

这里的base为VGG 16前13个卷积层构造,M代表maxpooling,C代表ceil_mode为True base = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512] 这里示例利用vgg(base, 3)即可实现基础网络的构造 vgg_base = vgg(base, 3)SSD采用了VGG 16作为基础网络,并在之上进行了一些改善,如图5.5所示。输入图像经过预处理后大小固定为300×300,首先经过VGG 16网络的前13个卷积层,然后利用两个卷积Conv 6与Conv 7取代了原来的全连接网络,进一步提取特征。

深度卷积层:

img

在VGG 16的基础上,SSD进一步增加了4个深度卷积层,用于更高语义信息的提取,如图所示。可以看出,Conv 8的通道数为512,而Conv 9、Conv 10与Conv 11的通道数都为256。从Conv 7到Conv 11,这5个卷积后输出特征图的尺寸依次为19×19、10×10、5×5、3×3和1×1。 为了降低参数量,在此使用了1×1卷积先降低通道数为该层输出通道数的一半,再利用3×3卷积进行特征提取。 利用PyTorch可以很方便地实现该深度卷积层,源代码文件为:

  1. # 额外的深度卷积层构造函数
  2. def add_extras(cfg, i, batch_norm=False):
  3.  # Extra layers added to VGG for feature scaling
  4.  layers = []
  5.  in_channels = i
  6.  flag = False
  7.  for k, v in enumerate(cfg):
  8.    if in_channels != 'S':
  9.      if v == 'S':
  10.        layers += [nn.Conv2d(in_channels, cfg[k + 1],
  11.              kernel_size=(1, 3)[flag], stride=2, padding=1)]
  12.      else:
  13.        layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]
  14.      flag = not flag
  15.    in_channels = v
  16.  return layers
  17. \# 额外部分的卷积通道数,S代表了步长为2,其余卷积层默认步长为1
  18. extras = [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256]
  19. \# 调用示例,直接调用add_extras函数即可完成该部分网络
  20. conv_extras = add_extras(extras, 1024)

先验框与边框特征提取网络

与Faster RCNN的Anchor类似,SSD采用了PriorBox来进行区域生成。不同的是,Faster RCNN首先在第一个阶段对固定的Anchor进行了位置修正与筛选,得到感兴趣区域后,在第二个阶段再对该区域进行分类与回归,而SSD直接将固定大小宽高的PriorBox作为先验的感兴趣区域,利用一个阶段完成了分类与回归。

img

PriorBox本质上是在原图上的一系列矩形框,如图所示。某个特征图上的一个点根据下采样率可以得到在原图的坐标,SSD先验性地提供了以该坐标为中心的4个或6个不同大小的PriorBox,然后利用特征图的特征去预测这4个PriorBox的类别与位置偏移量。

FastRCNN存在缺点:

在Faster RCNN中,所有Anchors对应的特征都来源于同一个特征图,而该层特征的感受野相同,很难处理被检测物体的尺度变化较大的情况,多个大小宽高的Anchors能起到的作用也有限。 从前面的思考可以得出,在深度卷积网络中,浅层的特征图拥有较小的感受野,深层的特征图拥有较大的感受野,因此SSD充分利用了这个特性,使用了多层特征图来做物体检测,浅层的特征图检测小物体,深层的特征图检测大物体,如下图所示。 *

img

从图中可以看出,SSD使用了第4、7、8、9、10和11这6个卷积层得到的特征图,这6个特征图尺寸越来越小,而其对应的感受野越来越大6个特征图上的每一个点分别对应4、6、6、6、4、4个PriorBox接下来分别利用3×3的卷积,即可得到每一个PriorBox对应的类别与位置预测量。 举个例子,第8个卷积层得到的特征图大小为10×10×512,每个点对应6个PriorBox,一共有600个PriorBox。由于采用的PASCAL VOC数据集的物体类别为21类,因此3×3卷积后得到的类别特征维度为6×21=126,位置特征维度为6×4(乘以4是因为有四个位置的框)=24。 如何确定每一个特征图PriorBox的具体大小呢?由于越深的特征图拥有的感受野越大,因此其对应的PriorBox也应越来越大,SSD采用了公式(5-1)来计算每一个特征图对应的PriorBox的尺度。

img

公式中K的取值为1、2、3、4、5、6,分别对应着SSD中的第4、7、8、9、10、11个卷积层。Sk代表这一层对应的尺度,Smin为0.2,Smax为0.9,分别表示最浅层与最深层对应的尺度与原图大小的比例,即第4个卷积层得到的特征图对应的尺度为0.2,第11个卷积层得到的特征图对应的尺度为0.9。 下面利用代码详细介绍如何生成每一层所需的PriorBox,代码如下:

img

img

有了多层的特征图,利用3×3卷积即可完成上面所述的提取过程。下面为所有边框特征提取的PyTorch实现。代码:

  1. def multibox(vgg, extra_layers, cfg, num_classes):
  2.  loc_layers = []
  3.  conf_layers = []
  4.  vgg_source = [21, -2]
  5.  for k, v in enumerate(vgg_source):
  6.    loc_layers += [nn.Conv2d(vgg[v].out_channels,
  7.                    cfg[k] * 4, kernel_size=3, padding=1)]
  8.    conf_layers += [nn.Conv2d(vgg[v].out_channels,
  9.              cfg[k] * num_classes, kernel_size=3, padding=1)]
  10.  for k, v in enumerate(extra_layers[1::2], 2):
  11.    loc_layers += [nn.Conv2d(v.out_channels, cfg[k]
  12.                    \* 4, kernel_size=3, padding=1)]
  13.    conf_layers += [nn.Conv2d(v.out_channels, cfg[k]
  14.                    \* num_classes, kernel_size=3, padding=1)]
  15.  return vgg, extra_layers, (loc_layers, conf_layers)
  16. \# 每个特征图上一个点对应的PriorBox数量
  17. mbox = [4, 6, 6, 6, 4, 4]
  18. \# 利用上面的vgg_base与conv_extras网络,生成类别与位置预测网络head_
  19. base_, extras_, head_ = multibox(vgg_base, conv_extras, mbox, num_classes)

综上所述,这个介绍一方面生成了共计8732个PriorBox的位置信息,同时也利用卷积网络提取了这8732个PriorBox的特征。

总体网络的前向计算过程

上面讲述了SSD的PriorBox与特征提取网络。为了更好地梳理网络的前向过程,下文将从代码角度讲述SSD网络的整个前向过程,以便理解起来更加清晰。代码:

  1. def forward(self, x):
  2.    \# sources保存特征图,loc与conf保存所有PriorBox的位置与类别预测特征
  3.    sources = list()
  4.    loc = list()
  5.    conf = list()
  6.    \# 对输入图像卷积到conv4_3,将特征添加到sources中
  7.    for k in range(23):
  8.      x = self.vgg[k](x)
  9.    s = self.L2Norm(x)
  10.    sources.append(s)
  11.    \# 继续卷积到Conv 7,将特征添加到sources中
  12.    for k in range(23, len(self.vgg)):
  13.      x = self.vgg[k](x)
  14.    sources.append(x)
  15.    \# 继续利用额外的卷积层计算,并将特征添加到sources中
  16.    for k, v in enumerate(self.extras):
  17.      x = F.relu(v(x), inplace=True)
  18.      if k % 2 == 1:
  19.        sources.append(x)
  20.    \# 对sources中的特征图利用类别与位置网络进行卷积计算,并保存到loc与conf中
  21.    for (x, l, c) in zip(sources, self.loc, self.conf):
  22.      loc.append(l(x).permute(0, 2, 3, 1).contiguous())
  23.      conf.append(c(x).permute(0, 2, 3, 1).contiguous())
  24.    loc = ![img](C:\Users\lds\AppData\Roaming\Tencent\QQTempSys\%W@GJ$ACOF(TYDYECOKVDYB.png)torch.cat([o.view(o.size(0), -1) for o in loc], 1)
  25.    conf = ![img](file:///C:\Users\lds\AppData\Roaming\Tencent\QQTempSys\%W@GJ$ACOF(TYDYECOKVDYB.png)torch.cat([o.view(o.size(0), -1) for o in conf], 1)
  26.    \# 对于训练来说,output包括了loc与conf的预测值及PriorBox的信息
  27.    output = (
  28.      loc.view(loc.size(0), -1, 4),
  29.      conf.view(conf.size(0), -1, self.num_classes),
  30.      self.priors
  31.   )
  32.    return output

损失函数计算与先验框匹配:

上一节的卷积网络得到了所有PriorBox的预测值与边框位置,为了得到最终的结果,还需要进行边框的匹配及损失计算。 SSD的这部分网络后处理可以分为4步:

  • 首先按照一定的原则,对所有的PriorBox赋予正、负样本的标签,并确定对应的真实物体标签,以方便后续损失的计算;

  • 有了对应的真值后,即可计算框的定位损失,这部分只需要正样本即可;

  • 同时,为了克服正、负样本的不均衡,进行难样本挖掘,筛选出数量是正样本3倍的负样本;

  • 最后,计算筛选出的正、负样本的类别损失,完成整个网络向前计算的全过程。

  1. 预选框与真实框的匹配

在求得8732个PriorBox坐标及对应的类别、位置预测后,首先要做的就是为每一个PriorBox贴标签,筛选出符合条件的正样本与负样本,以便进行后续的损失计算。判断依据与Faster RCNN相同,都是通过预测与真值的IoU值来判断。 SSD处理匹配过程时遵循以下4个原则:

  • 在判断正、负样本时,IoU阈值设置为0.5,即一个PriorBox与所有真实框的最大IoU小于0.5时,判断该框为负样本。(越大越严格!!)

  • 判断对应关系时,将PriorBox与其拥有最大IoU的真实框作为其位置标签。

  • 与真实框有最大IoU的PriorBox,即使该IoU不是此PriorBox与所有真实框IoU中最大的IoU,也要将该Box对应到真实框上,这是为了保证真实框的Recall。

  • 在预测边框位置时,SSD与Faster RCNN相同,都是预测相对于预选框的偏移量,因此在求得匹配关系后还需要进行偏移量计算,具体公式参照上面。 具体的匹配过程代码如下;主要是下面的match函数:

    1. \# 输入包括IoU阈值、真实边框位置、预选框、方差、真实边框类别
    2. \# 输出为每一个预选框的类别,保存在conf_t中,对应的真实边框位置保存在loc_t中
    3. def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
    4.  \# 注意,这里的truth是最大或最小值的形式,而prior是中心点与宽高形式
    5.  \# 求取真实框与预选框的IoU
    6.  overlaps = jaccard(truths, point_form(priors))
    7. ......
    8.  \# 将每一个真实框对应的最佳PriorBox的IoU设置为2,确保是最优的PriorBox
    9.  best_truth_overlap.index_fill_(0, best_prior_idx, 2)
    10.  \# 对于每一个真实框,其拥有最大IoU的PriorBox要对应到该真实框上,即使在这个PriorBox
    11.  \# 中该真实框不是最大的IoU,这是为了保证Recall
    12.  for j in range(best_prior_idx.size(0)):
    13.    best_truth_idx[best_prior_idx[j]] = j
    14.  \# 每一个PriorBox对应的真实框的位置
    15.  matches = truths[best_truth_idx]    
    16.  \# 每一个PriorBox对应的真实类别
    17.  conf = labels[best_truth_idx] + 1    
    18.  \# 如果一个PriorBox对应的最大IoU小于0.5,则视为负样本
    19.  conf[best_truth_overlap < threshold] = 0
    20.  \# 进一步计算定位的偏移真值
    21.  loc = encode(matches, priors, variances)
    22.  loc_t[idx] = loc
    23.  conf_t[idx] = conf

  • 定位损失计算

在完成匹配后,由于有了正、负样本及每一个样本对应的真实框,因此可以进行定位的损失计算。与Faster RCNN相同,SSD使用了smoothL10函数作为定位损失函数,并且只对正样本计算。具体公式可参见上面。

  1. \# 计算所有正样本的定位损失,负样本不需要定位损失
  2.    pos = conf_t > 0
  3.    num_pos = pos.sum(dim=1, keepdim=True)
  4.    \# 将pos_idx扩展为[32, 8732, 4],正样本的索引
  5.    pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
  6.    \# 正样本的定位预测值
  7.    loc_p = loc_data[pos_idx].view(-1, 4)
  8.    \# 正样本的定位真值
  9.    loc_t = loc_t[pos_idx].view(-1, 4)
  10.    \# 所有正样本的定位损失
  11.    loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)

  1. 难样本挖掘

在完成正、负样本匹配后,由于一般情况下一张图片的物体数量不会超过100,因此会存在大量的负样本。如果这些负样本都考虑则在损失反传时,正样本能起到的作用就微乎其微了,因此需要进行难样本的挖掘。这里的难样本是针对负样本而言的。 Faster RCNN通过限制正负样本的数量来保持正、负样本均衡,而在SSD中,则是保证正、负样本的比例来实现样本均衡。具体做法是在计算出所有负样本的损失后进行排序,选取损失较大的那一部分进行计算,舍弃剩下的负样本,数量为正样本的3倍。 具体实现如代码所示。在计算完所有边框的类别交叉熵损失后,难样本挖掘过程主要分为5步:

  • 首先过滤掉正样本;

  • 然后将负样本的损失排序;

  • 接着计算正样本的数量;

  • 进而得到负样本的数量;

  • 最后根据损失大小得到留下的负样本索引。

源代码文件见:

  1. \# 对于类别损失,进行难样本挖掘,控制比例为1:3
  2.    \# 所有PriorBox的类别预测量
  3.    batch_conf = conf_data.view(-1, self.num_classes)
  4.    \# 利用交叉熵函数,计算所有PriorBox的类别损失
  5.    loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
  6.    \# 接下来进行难样本挖掘,分为5步
  7.    loss_c = loss_c.view(pos.size()[0], pos.size()[1])
  8.    \# 1:首先过滤掉正样本
  9.    loss_c[pos] = 0 # filter out pos boxes for now
  10.    loss_c = loss_c.view(num, -1)
  11.    \# 2:将所有负样本的类别损失排序
  12.    _, loss_idx = loss_c.sort(1, descending=True)
  13.    \# idx_rank为排序后每个PriorBox的排名
  14.    _, idx_rank = loss_idx.sort(1)
  15.    \# 3:计算正样本的数量
  16.    num_pos = pos.long().sum(1, keepdim=True)
  17.    \# 4:控制正、负样本的比例为1∶3
  18.    num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
  19.    \# 5:选择每个batch中负样本的索引
  20.    neg = idx_rank < num_neg.expand_as(idx_rank)

  1. 类别损失计算

在得到筛选后的正、负样本后,即可进行类别的损失计算。SSD在此使用了交叉熵损失函数,并且正、负样本全部参与计算。交叉熵损失函数可前面。

  1. # 计算正、负样本的类别损失
  2.    \# 将正、负样本的索引扩展为[32, 8732, 21]格式
  3.    pos_idx = pos.unsqueeze(2).expand_as(conf_data)
  4.    neg_idx = neg.unsqueeze(2).expand_as(conf_data)
  5.    \# 把类别的预测值从所有的预测中提取出来
  6.    conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
  7.    \# 把类别的真值从所有真值中提取出来
  8.    targets_weighted = conf_t[(pos+neg).gt(0)]
  9.    loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
​
​

SSD总结(优缺点简述)

SSD实现了一个较为优雅、简洁的物体检测框架,使用了一阶网络即完成了物体检测任务,达到了同时期物体检测的较高水平。总体上,SSD主要有以下3个优点:

  • 由于利用了多层的特征图进行预测,因此虽然是一阶的网络,但在某些场景与数据集下,检测精度依然可以与Faster RCNN媲美。

  • 一阶网络的实现,使得其检测速度可以超过同时期的Faster RCNN及YOLO算法,对于速度要求较高的工程应用场景,SSD是一个很不错的选择。

  • 网络优雅,实现简单,没有太多的工程技巧,这也为后续的改善工作提拱了很大的空间。 与此同时,追求更高检测性能的脚步永不会停止,SSD算法也有以下3点限制:

  • 对于小物体的检测效果一般,这是由于其虽然使用了分辨率大的浅层特征图来检测小物体,但浅层的语义信息不足,无法很好地完成分类与回归的预测。

  • 每一层PriorBox的大小与宽高依赖于人工设置,无法自动学习,当检测任务更换时,调试过程较为烦琐。

  • 由于是一阶的检测算法,分类与边框回归都只有一次,在一些追求高精度的场景下,SSD系列相较于Faster RNCN系列来讲,仍然处在下风。

针对SSD的这些问题,后续的学者从多个角度探讨了提升SSD性能的策略,在此介绍4个较为经典的改进算法,分别是DSSD、RSSD、RefineDet及RFBNet算法,如图所示。

img

SSD(Pytorch)的代码地址:aaalds/SSD-pytorch: ssd的pytorch代码实现 (github.com)

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号