SSD(单发多框检测)

论文标题: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-RCNN Faster-RCNN Mask-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 原来的两个全连接层fc6 fc7 ,改成了卷积层 Conv6 Conv7

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.代码示例

python 复制代码
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)提供了重要设计思路,至今仍是平衡速度与精度的经典方案。

相关推荐
Mr数据杨2 小时前
【CanMV K210】传感器实验 光敏传感器环境光照强度采集
人工智能·硬件开发·canmv k210
Geometry Fu2 小时前
《智能终端与边缘计算》第一章 概述
人工智能·边缘计算
有一个好名字2 小时前
CrewAI 入门指南01:从零开始构建多 Agent 协作系统
人工智能
吴佳浩2 小时前
用 Stitch 实现 AI 前端工程化:找回消失的UI美学(别再 Vibe 瞎Coding 了)
前端·人工智能·llm
生成论实验室2 小时前
Transformer架构上的语言模型自已评判“判断力缺失”
人工智能·深度学习·语言模型·自然语言处理·transformer
ฅ ฅBonnie2 小时前
Hermes 与 Cloud Code/OpenClaw 架构对比分析及部署实践
人工智能·ai·架构·ai编程
ZHANG8023ZHEN2 小时前
Diffusion 数学推理
人工智能·python·机器学习
实在智能RPA2 小时前
实在Agent针对金融行业Agent灾备与高可用是如何进行设计的?深度拆解金融级智能体的架构安全与连续性保障
人工智能·安全·ai·金融·架构
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章78-KRT测量
图像处理·人工智能·数码相机·opencv·算法·计算机视觉
Szime2 小时前
AI服务器电源、充电桩、储能BMS项目,电子元器件BOM配单怎么做更高效?
运维·服务器·人工智能