目录

单发多框检测(SSD)

模型

/posts/learning/cs/cv/ssd/1.png 此模型主要由基础网络组成,其后是几个多尺度特征块。 基本网络用于从输入图像中提取特征,因此它可以使用深度卷积神经网络。 单发多框检测论文中选用了在分类层之前截断的VGG ,现在也常用ResNet替代。 我们可以设计基础网络,使它输出的高和宽较大。 这样一来,基于该特征图生成的锚框数量较多,可以用来检测尺寸较小的目标。 接下来的每个多尺度特征块将上一层提供的特征图的高和宽缩小(如减半),并使特征图中每个单元在输入图像上的感受野变得更广阔。

由于接近顶部的多尺度特征图较小,但具有较大的感受野,它们适合检测较少但较大的物体

[类别预测层]

设目标类别的数量为$q$。这样一来,锚框有$q+1$个类别,其中0类是背景。 在某个尺度下,设特征图的高和宽分别为$h$和$w$。 如果以其中每个单元为中心生成$a$个锚框,那么我们需要对$hwa$个锚框进行分类。 如果使用全连接层作为输出,很容易导致模型参数过多。

具体来说,类别预测层使用一个保持输入高和宽的卷积层。 这样一来,输出和输入在特征图宽和高上的空间坐标一一对应。 考虑输出和输入同一空间坐标($x$、$y$):输出特征图上($x$、$y$)坐标的通道里包含了以输入特征图($x$、$y$)坐标为中心生成的所有锚框的类别预测。 因此输出通道数为$a(q+1)$,其中索引为$i(q+1) + j$($0 \leq j \leq q$)的通道代表了索引为$i$的锚框有关类别索引为$j$的预测。

在下面,我们定义了这样一个类别预测层,通过参数num_anchorsnum_classes分别指定了$a$和$q$。 该图层使用填充为1的$3\times3$的卷积层。此卷积层的输入和输出的宽度和高度保持不变。

(边界框预测层)

边界框预测层的设计与类别预测层的设计类似。 唯一不同的是,这里需要为每个锚框预测4个偏移量,而不是$q+1$个类别。

[连结多尺度的预测]

正如我们所提到的,单发多框检测使用多尺度特征图来生成锚框并预测其类别和偏移量。 在不同的尺度下,特征图的形状或以同一单元为中心的锚框的数量可能会有所不同。 因此,不同尺度下预测输出的形状可能会有所不同。

在以下示例中,我们为同一个小批量构建两个不同比例(Y1Y2)的特征图,其中Y2的高度和宽度是Y1的一半。 以类别预测为例,假设Y1Y2的每个单元分别生成了$5$个和$3$个锚框。 进一步假设目标类别的数量为$10$,对于特征图Y1Y2,类别预测输出中的通道数分别为$5\times(10+1)=55$和$3\times(10+1)=33$,其中任一输出的形状是(批量大小,通道数,高度,宽度)。

[高和宽减半块]

为了在多个尺度下检测目标,我们在下面定义了高和宽减半块down_sample_blk,该模块将输入特征图的高度和宽度减半。 事实上,该块应用了在 :numref:subsec_vgg-blocks中的VGG模块设计。 更具体地说,每个高和宽减半块由两个填充为$1$的$3\times3$的卷积层、以及步幅为$2$的$2\times2$最大汇聚层组成。 我们知道,填充为$1$的$3\times3$卷积层不改变特征图的形状。但是,其后的$2\times2$的最大汇聚层将输入特征图的高度和宽度减少了一半。 对于此高和宽减半块的输入和输出特征图,因为$1\times 2+(3-1)+(3-1)=6$,所以输出中的每个单元在输入上都有一个$6\times6$的感受野。因此,高和宽减半块会扩大每个单元在其输出特征图中的感受野。

模型构建

class TinySSD(nn.Module):
    def __init__(self, num_classes, **kwargs):
        super(TinySSD, self).__init__(**kwargs)
        self.num_classes = num_classes
        idx_to_in_channels = [64, 128, 128, 128, 128]
        for i in range(5):
            # 即赋值语句self.blk_i=get_blk(i)
            setattr(self, f'blk_{i}', get_blk(i))
            setattr(self, f'cls_{i}', cls_predictor(idx_to_in_channels[i],
                                                    num_anchors, num_classes))
            setattr(self, f'bbox_{i}', bbox_predictor(idx_to_in_channels[i],
                                                      num_anchors))

    def forward(self, X):
        anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
        for i in range(5):
            # getattr(self,'blk_%d'%i)即访问self.blk_i
            X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
                X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
                getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
        anchors = torch.cat(anchors, dim=1)
        cls_preds = concat_preds(cls_preds)
        cls_preds = cls_preds.reshape(
            cls_preds.shape[0], -1, self.num_classes + 1)
        bbox_preds = concat_preds(bbox_preds)
        return anchors, cls_preds, bbox_preds

[定义损失函数和评价函数]

目标检测有两种类型的损失。 第一种有关锚框类别的损失:我们可以简单地复用之前图像分类问题里一直使用的交叉熵损失函数来计算; 第二种有关正类锚框偏移量的损失:预测偏移量是一个回归问题。 但是,对于这个回归问题,我们在这里不使用 :numref:subsec_normal_distribution_and_squared_loss中描述的平方损失,而是使用$L_1$范数损失,即预测值和真实值之差的绝对值。 掩码变量bbox_masks令负类锚框和填充锚框不参与损失的计算。 最后,我们将锚框类别和偏移量的损失相加,以获得模型的最终损失函数。

cls_loss = nn.CrossEntropyLoss(reduction='none')
bbox_loss = nn.L1Loss(reduction='none')

def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
    cls = cls_loss(cls_preds.reshape(-1, num_classes),
                   cls_labels.reshape(-1)).reshape(batch_size, -1).mean(dim=1)
    bbox = bbox_loss(bbox_preds * bbox_masks,
                     bbox_labels * bbox_masks).mean(dim=1)
    return cls + bbox

由于偏移量使用了$L_1$范数损失,我们使用平均绝对误差来评价边界框的预测结果。这些预测结果是从生成的锚框及其预测偏移量中获得的。

def cls_eval(cls_preds, cls_labels):
    # 由于类别预测结果放在最后一维,argmax需要指定最后一维。
    return float((cls_preds.argmax(dim=-1).type(
        cls_labels.dtype) == cls_labels).sum())

def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
    return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())

训练模型

num_epochs, timer = 20, d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                        legend=['class error', 'bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
    # 训练精确度的和,训练精确度的和中的示例数
    # 绝对误差的和,绝对误差的和中的示例数
    metric = d2l.Accumulator(4)
    net.train()
    for features, target in train_iter:
        timer.start()
        trainer.zero_grad()
        X, Y = features.to(device), target.to(device)
        # 生成多尺度的锚框,为每个锚框预测类别和偏移量
        anchors, cls_preds, bbox_preds = net(X)
        # 为每个锚框标注类别和偏移量
        bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)
        # 根据类别和偏移量的预测和标注值计算损失函数
        l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
                      bbox_masks)
        l.mean().backward()
        trainer.step()
        metric.add(cls_eval(cls_preds, cls_labels), cls_labels.numel(),
                   bbox_eval(bbox_preds, bbox_labels, bbox_masks),
                   bbox_labels.numel())
    cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
    animator.add(epoch + 1, (cls_err, bbox_mae))
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on '
      f'{str(device)}')

小结

  • 单发多框检测是一种多尺度目标检测模型。基于基础网络块和各个多尺度特征块,单发多框检测生成不同数量和不同大小的锚框,并通过预测这些锚框的类别和偏移量检测不同大小的目标。
  • 在训练单发多框检测模型时,损失函数是根据锚框的类别和偏移量的预测及标注值计算得出的。