论文标题:SSD: Single Shot MultiBox Detector

地址:https://arxiv.org/pdf/1512.02325 

        SSD(Single Shot MultiBox Detector)是一种高效的目标检测算法,其核心思想是在单次前向传播中同时预测目标的类别和位置。相较于传统的两阶段检测方法(如Faster R-CNN),SSD通过结合多尺度特征图与预定义先验框(Prior Boxes),实现了速度与精度的平衡。

       为了改进R-CNN,后续出现Fast-RCNNFaster-RCNNMask-RCNN一系列改进算法。这些算法虽然取得了更高的准确率,但是在速度方面还是有所欠缺。SSD就恰巧解决了这一点,SSD基本可以达到实时检测的速度要求,并且在准确性和计算速度方面具有良好的平衡。

        SSD:Single Shot(单步):目标定位和分类在网络的单个前向传递中完成;MultiBox:边界框回归技术;Detector(检测器):对检测到的对象进行分类。

        SSD 在一张图像的多个不同尺度特征图上,同时预测所有目标的类别和边界框,一步完成检测,没有单独的区域建议阶段。核心设计:1.单步检测:没有 RPN,前向传播一次直接输出所有目标的类别和坐标;2.多尺度特征图预测:用不同大小的特征图检测不同大小的目标;3.密集先验框:每个特征点生成多个不同尺度、宽高比的先验框。

        1.经典的SSD300(输入 300×300 图像)

        SSD300架构基于VGG-16构建,使用VGG的前五个卷积,Conv6层以上使用辅助卷积层而不是全连接层。使用VGG-16作为基础网络的原因是其高质量的图像分类和迁移学习以改善结果。使用辅助卷积层,我们可以提取多个尺度的特征,并逐步减小每个后续层的尺寸。越往前的feature map层(越大的feature map)让它检测越小的物体,越靠后的层(越小的feature map)检测比较大的物体。

        例如,这个较小的猫就适合在比较大的8×8feature map层进行检测,这个狗比较大,所以它就适合在比较小的4×4层进行检测。

        VGG16 原本的结构是:5个卷积块和3个全连接层(fc6、fc7、fc8)。而SSD 为了做成全卷积网络,把 VGG 原来的两个全连接层fc6fc7,改成了卷积层Conv6Conv7

VGG16架构

SSD300(Conv5_3是第5个卷积块的第3个卷积层遵循VGG 系列网络标准层命名规则)

层名称

类型

输出尺寸

作用

输入

-

300×300×3

输入RGB彩色图像

VGG16 骨干最终输出就是Conv5_3

卷积块

19×19×512

提取基础特征

Conv4_3

VGG 原生卷积

38×38×512

检测小目标

Conv5_3

VGG 原生卷积

19×19×512

基础特征输出

Conv6

3×3 膨胀卷积(VGG改造层)

19×19×1024

扩大感受野

Conv7

1×1 卷积(VGG改造层)

19×19×1024

检测中小目标

Conv8_1 + Conv8_2

1×1+3×3卷积(额外)

10×10×512

检测中目标

Conv9_1 + Conv9_2

1×1+3×3卷积(额外)

5×5×256

检测中大目标

Conv10_1 + Conv10_2

1×1+3×3卷积(额外)

3×3×256

检测大目标

Conv11_1 + Conv11_2

1×1+3×3卷积(额外)

1×1×256

检测超大目标

所有检测头输出拼接

-

(8732, num_classes + 4)

汇总6个特征图的所有检测结果

NMS后处理

-

(N, num_classes + 5)

去除重复检测框

最终检测结果

-

(K, 6)

输出K个目标:[x1,y1,x2,y2, 置信度,类别]

        所有的 "额外卷积层" 其实都是两层一组(1×1降维 + 3×3卷积)。其中,Conv6和Conv7共同构成了第一个额外特征块,最终输出的是Conv7特征图。Conv6用的是3×3膨胀卷积(dilation=6,这是SSD的一个关键设计,用来在不降低特征图分辨率的情况下,大幅扩大感受野。

      所有检测头输出拼接的输出尺寸是(8732, num_classes + 4):8732 个先验框;num_classes:数据集类别数(如 VOC2007 是 20 类);+4:每个先验框的 4 个坐标偏移量。

2. SSD 算法核心的锚框(Anchor)机制原理

        SSD 在图像的不同位置、不同尺度上,预先定义好大量不同大小、不同形状的 "模板框"(锚框),网络不需要从零开始预测物体位置,只需要预测这些模板框和真实物体之间的偏移量,同时预测每个框里有没有物体、是什么物体。

        对每个像素生成多个以它为中心的多个锚框,给定n个锚框尺度(大小)参数s1,...,sn和m个宽高比,就是生成n+m−1个锚框,其大小和高宽比分别为:(s1,r1),(s2,r2),....(sn,r1),(s1,r2),....(s1,rm)。

        上图的网格是特征图的网格(比如 38×38、19×19 的特征图);每个网格交叉点(特征点),都会生成多个以它为中心的彩色框,这些就是锚框;图里每个点生成了6个锚框,对应SSD标准设计:3种大小 + 3种宽高比。

        3.模型

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

SSD 完整的检测流程(这是一个多尺度目标检测模型)

        SSD的所有预测都是基于预先定义好的锚框进行的:网络不直接预测物体的绝对坐标,只预测锚框到真实物体框的偏移量;网络同时预测每个锚框包含物体的类别概率;每个锚框对应独立的类别预测分支独立的边界框预测分支。

        SSD 所有特征图的检测头都采用完全相同的设计两个并行的 3×3 卷积层。

        3×3卷积(分类分支)→ 输出类别概率

        3×3卷积(回归分支)→ 输出边界框偏移量

        (1)类别预测(分类分支)

        类别分支输出的是每个锚框属于各个类别的概率。例如,对于VOC2007数据集,每个锚框输出21个概率值:第0个值:属于背景的概率;第1-20个值:分别属于20个物体类别的概率。

        (2)边界框预测(回归分支)

        边界框分支输出的不是最终的框坐标 (x1,y1,x2,y2),而是4个偏移量 (dx, dy, dw, dh),表示锚框需要如何调整才能对准真实物体。用偏移量不直接预测坐标得原因:

        训练更稳定:偏移量的数值范围很小(通常在- 1到1之间),比直接预测几百像素的坐标更容易收敛。

        泛化能力强:网络学习的是相对位置关系,而不是绝对坐标,对不同大小的物体都适用。

        符合锚框设计思想:锚框已经提供了很好的初始位置,网络只需要做微小调整。

        SSD 算法的核心原理总结:

        (1)输入图像之后,首先进入一个基础网络来抽取特征,抽取完特征之后对每个像素生成大量的锚框(每个锚框就是一个样本,然后预测锚框的类别以及到真实边界框的偏移)。锚框不是网络学习的,是预先定义好的,网络学习的只是锚框的偏移量,不是锚框本身的位置和大小。

        (2)SSD 在给定锚框之后直接对锚框进行预测,而不需要做两阶段(SSD 通过做不同分辨率下的预测来提升最终的效果)。

        (3)SSD 不再使用 RPN 网络,而是直接在生成的大量样本(锚框)上做预测,看是否包含目标物体;如果包含目标物体,再预测该样本到真实边缘框的偏移。

对比维度

SSD

Faster R-CNN

预测阶段

单阶段,直接在锚框上做最终预测

两阶段,先 RPN 生成候选框,再对候选框做第二次预测

预测对象

所有锚框(8732个)

筛选后的候选框(2000个)

分类分支

直接预测所有物体类别

先二分类(前景/背景),再多分类

速度

小目标检测

较好(多尺度特征)

更好(两阶段精细检测)

4.代码示例

import torch
import torch.nn as nn
import torchvision.models as models
import numpy as np

# L2归一化层
'''
SSD论文专属层
作用:Conv4_3特征图数值范围太大,归一化后才能稳定训练
'''
class L2Norm(nn.Module):
    def __init__(self, in_channels=512, scale=20):
        super().__init__()
        # 可学习的缩放参数,初始值20(论文固定值)
        self.gamma = nn.Parameter(torch.ones(in_channels) * scale)
        self.eps = 1e-10

    def forward(self, x):
        # L2归一化公式:x / 模长 * 缩放系数
        norm = x.pow(2).sum(dim=1, keepdim=True).sqrt() + self.eps
        return x / norm * self.gamma.view(1, -1, 1, 1)

# SSD300模型
class SSD300(nn.Module):
    def __init__(self, num_classes=21):
        super().__init__()
        # 固定参数 6个特征图的锚框数量 VOC数据集:20类+背景=21类
        self.num_anchors = [4, 6, 6, 6, 4, 4]
        self.num_classes = num_classes

        # 加载VGG16并修改池化层
        vgg = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1).features
        # 修改所有池化层的ceil_mode=True,确保尺寸正确 保证300×300输入 → 特征图尺寸严格为[38,19,10,5,3,1]
        for layer in vgg:
            if isinstance(layer, nn.MaxPool2d):
                layer.ceil_mode = True

        # 去掉VGG的池化层
        self.vgg = nn.ModuleList(list(vgg.children())[:-1])
        # 给Conv4_3特征图加归一化
        self.l2_norm = L2Norm()

        # 额外特征层(官方结构)
        # 作用:在VGG后新增4层,生成小尺寸特征图(检测大目标)
        self.extras = nn.ModuleList([
            # Conv6 → Conv7(VGG改造,生成19×19特征图)
            nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6),
            nn.ReLU(inplace=True),
            nn.Conv2d(1024, 1024, kernel_size=1),
            nn.ReLU(inplace=True),
            # Conv8(生成10×10特征图) 额外层1
            nn.Conv2d(1024, 256, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1),
            nn.ReLU(inplace=True),
            # Conv9(生成5×5特征图) 额外层2
            nn.Conv2d(512, 128, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1),
            nn.ReLU(inplace=True),
            # Conv10(生成3×3特征图) 额外层3
            nn.Conv2d(256, 128, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 256, kernel_size=3),
            nn.ReLU(inplace=True),
            # Conv11(生成1×1特征图) 额外层4
            nn.Conv2d(256, 128, kernel_size=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 256, kernel_size=3),
            nn.ReLU(inplace=True),
        ])

        # 检测头
        # 作用:对6个特征图分别预测 边界框 + 类别

        # 边界框预测:每个锚框输出4个值 (x,y,w,h偏移量)
        self.loc_layers = nn.ModuleList([
            nn.Conv2d(512, 4*4, 3, padding=1),   # 特征图0:4锚框 → 16通道 对应conv4_3
            nn.Conv2d(1024, 6*4, 3, padding=1),  # 特征图1:6锚框 → 24通道 对应conv6+conv7
            nn.Conv2d(512, 6*4, 3, padding=1),   # 特征图2:6锚框 → 24通道 对应conv8_2
            nn.Conv2d(256, 6*4, 3, padding=1),   # 特征图3:6锚框 → 24通道 对应conv9_2
            nn.Conv2d(256, 4*4, 3, padding=1),   # 特征图4:4锚框 → 16通道 对应conv10_2
            nn.Conv2d(256, 4*4, 3, padding=1),   # 特征图5:4锚框 → 16通道 对应conv11_2
        ])

        # 类别预测:每个锚框输出21个概率(VOC21类)
        self.conf_layers = nn.ModuleList([
            nn.Conv2d(512, 4*num_classes, 3, padding=1),
            nn.Conv2d(1024, 6*num_classes, 3, padding=1),
            nn.Conv2d(512, 6*num_classes, 3, padding=1),
            nn.Conv2d(256, 6*num_classes, 3, padding=1),
            nn.Conv2d(256, 4*num_classes, 3, padding=1),
            nn.Conv2d(256, 4*num_classes, 3, padding=1),
        ])

    def forward(self, x):
        features = [] # 存放6个检测特征图

        # 提取Conv4_3(现在尺寸是38x38) 提取第0个特征图(38×38,检测小目标)
        for i in range(23):
            x = self.vgg[i](x)
        features.append(self.l2_norm(x))

        # 提取VGG剩余部分 提取第1个特征图(19×19,检测中目标)
        for i in range(23, len(self.vgg)):
            x = self.vgg[i](x)

        # 提取额外特征 提取剩余4个特征图(10×10 /5×5 /3×3 /1×1)
        for i, layer in enumerate(self.extras):
            x = layer(x)
            # 只保留每个块的最后输出(共4个)
            if i in [3, 7, 11, 15, 19]:
                features.append(x)

        # 调试打印
        print("\n===== 每个特征图的锚框数 =====")
        total = 0
        for idx, feat in enumerate(features):
            h, w = feat.size(2), feat.size(3)
            num_anchors = self.num_anchors[idx]
            feat_anchors = h * w * num_anchors
            print(f"特征图{idx} ({feat.shape}): {feat_anchors} 个锚框")
            total += feat_anchors
        print(f"模型输出总锚框数: {total}")

        # 预测输出
        locs = []  # 存放所有边界框预测
        confs = []  # 存放所有类别预测
        for i in range(6):
            # 预测边界框 + 维度变换(把特征图展平)
            loc = self.loc_layers[i](features[i])
            loc = loc.permute(0, 2, 3, 1).contiguous().view(x.size(0), -1)
            locs.append(loc)

            # 预测类别 + 维度变换
            conf = self.conf_layers[i](features[i])
            conf = conf.permute(0, 2, 3, 1).contiguous().view(x.size(0), -1)
            confs.append(conf)

        # 拼接所有结果 → 输出标准形状
        return torch.cat(locs, dim=1).view(x.size(0), -1, 4), \
               torch.cat(confs, dim=1).view(x.size(0), -1, self.num_classes)

# 锚框生成函数(固定8732个)
def generate_anchors():
    # 标准参数
    feature_map_sizes = [38, 19, 10, 5, 3, 1]  # 6个特征图尺寸
    steps = [8, 16, 32, 64, 100, 300]  # 感受野步长
    scales = [0.1, 0.2, 0.375, 0.55, 0.725, 0.9, 1.05]  # 锚框大小
    aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]]  # 宽高比

    anchors = []
    # 遍历每个特征图,生成所有锚框
    for k, f in enumerate(feature_map_sizes):
        for i in range(f):
            for j in range(f):
                # 计算锚框中心坐标
                cx = (j + 0.5) * steps[k] / 300.0
                cy = (i + 0.5) * steps[k] / 300.0
                # 生成不同大小/形状的锚框
                s = scales[k]
                anchors.append([cx, cy, s, s])

                s_prime = np.sqrt(scales[k] * scales[k + 1])
                anchors.append([cx, cy, s_prime, s_prime])

                for ar in aspect_ratios[k]:
                    anchors.append([cx, cy, s * np.sqrt(ar), s / np.sqrt(ar)])
                    anchors.append([cx, cy, s / np.sqrt(ar), s * np.sqrt(ar)])

    return torch.tensor(anchors, dtype=torch.float32)

# 4. 测试运行
if __name__ == "__main__":
    model = SSD300(num_classes=21)

    anchors = generate_anchors()
    print(f"锚框生成成功,总数:{len(anchors)}")

    test_input = torch.randn(1, 3, 300, 300)  # 输入1张300×300图片
    with torch.no_grad():
        locs, confs = model(test_input)

    print(f"边界框输出形状: {locs.shape}")  # [批次大小, 锚框总数, 预测特征数]
    print(f"类别输出形状: {confs.shape}")  # [批次大小, 锚框总数, VOC 数据集的总类别数] (SSD300 默认训练数据集)

        运行结果如下:

5.优势与局限

  • 优势:实时性强(如VGG-SSD在Titan X上达59 FPS),对小目标检测效果较好。

  • 局限:极端长宽比目标易漏检,深层特征分辨率低影响小目标精度。

        SSD为后续算法(如YOLO、RetinaNet)提供了重要设计思路,至今仍是平衡速度与精度的经典方案。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐