二、SSD网络原理及代码讲解

二、SSD网络原理及代码讲解二、SSD论文导读全称:SingleShotMultiBoxDetector说明:仅需要单个神经网络的目标检测方法,可以在不同分辨率的特征图下进行预测;论文概要1、多尺度特征图用大特征图来检测小目标,小特征图来检测大目标(同YOLOv3)2、卷积检测直接使用卷积输出边框回归和类别预测,好处就是减少了计算量(同YOLOv3)如上图所示,用于预测的featuremap是5x5x256,anchorbox为3,一共生成5x5x3=75锚框;用于定位的卷积层输出为5x5x(4×3),

二、SSD

论文导读

全称:Single Shot MultiBox Detector

说明:仅需要单个神经网络的目标检测方法,可以在不同分辨率的特征图下进行预测;

论文概要

1、多尺度特征图

用大特征图来检测小目标,小特征图来检测大目标(同YOLOv3)

2、卷积检测

直接使用卷积输出边框回归和类别预测,好处就是减少了计算量(同YOLOv3)

在这里插入图片描述

如上图所示,用于预测的feature map是5x5x256,anchor box为3,一共生成5x5x3=75锚框;

用于定位的卷积层输出为5x5x(4×3),因为预测框需要x,y,w,h四个通道表示;

用于分类的卷积层输出为5x5x(21×3),因为VOC分类有21个类别,一个锚框需要21个通道;

3、先验框

借鉴Faster R-CNN的思想,设置尺度或长宽比不同的先验框;

对于先验框的长宽比,一般取(1,2,3,1/2,1/3)

在这里插入图片描述

4、模型结构

基础模型是采用VGG-16

4MP0Zn.png

细节部分:

  • VGG16的全连接层fc6和fc7转换成3×3卷积层conv6和1×1卷积层conv7;
  • 池化层由stride=2的2×2变成stride=1的3×3;
  • 移除dropout和fc8,新增一系列卷积层做finetuning;

5、Atrous算法(空洞卷积)

在Conv6和Conv7中使用,主要作用是增大了感受野,相比传统卷积需要参数减少;

6、特征图

从Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2作为检测所用的特征图,加上Conv4_3层,共提取了六个特征图;

大小分别为38×38、19×19、10×10、5×5、3×3、1×1;

7、先验框匹配

原则1:对于图像中的每一个ground truth,找到与其IOU最大的先验框;

原则二:对于剩余未匹配的先验框,若与某个ground truthIOU高于阈值(0.5),那么也进行匹配;

Hard Nagative Mining(难例训练):

为了保证正负样本尽量平衡,对负样本进行抽样,抽样室按照先验框与背景的置信度误差进行降序排列,选取误差较大的作为训练负样本,保证正负样本比例接近1:3;

8、损失计算流程

在这里插入图片描述

9、数据增强

主要采用水平翻转、随机裁剪加颜色扭曲、随机采集块域(获取小目标训练样本)

10、预测过程

  • 对于每个预测框,首先根据置信度最大者确定其类别,并过滤掉属于背景的预测框;
  • 根据置信度阈值(如0.5)过滤掉低于阈值的预测框,对留下的预测框进行解码,得到真实的位置参数,一版还需要做clip,防止预测框超出原始图像大小;
  • 解码之后,一版根据置信度进行降序排列,仅保留top-k个预测框;
  • 最后进行NMS算法,过滤掉重叠度较大的预测框;

11、性能评估

数据扩增很重要,对map提升很大;

使用不同的长宽比的先验框可以得到更好的结果;

12、与Faster R-CNN和YOLO-V1系列做比较

在这里插入图片描述

SSD的速度大大超过Faster R-CNN和YOLO-V1,原因如下:

1、首先SSD是一个单阶段网路,相比于Faster R-CNN这个双阶段网络少了一步,自然会快很多,并且SSD输入特征图大小远远小于Faster R-CNN输入的特征图大小;

2、YOLO网络虽然看起来比SSD网络简单,但V1版本中含有大量全连接层,计算量会大很多;

3、SSD调整了VGG的网络结构,将两层全连接层替换为卷积层,参数减少;

4、使用了空洞卷积的算法,论文中表示该算法可以提速20%;

论文总结

关键点:

1、多特征图检测;

2、先验框的设置;

3、VGG16的改造;

创新点:

1、兼顾速度和精度;

2、多尺度特征图;

启发点:

1、如何利用更浅层的特征图(SSD中只从4-3层开始使用特征图);

2、在更深的网络如ResNet-101中如何定义Default box;

3、Anchor定义的多尺度是否能满足实际物体尺度变化要求(在某些任务中无法满足);

论文代码

一、Pytorch数据集加载

Pytorch提供了Dataset的基本类,并在torchvision中提供了较多数据变换函数,见下图:

在这里插入图片描述

下面讲解每一步的代码操作:

1、继承Dataset类

from torch.utils.data import Dataset

class my_data(Dataset):
    def __init__(self, image_path, annotation_path, transform=None):
        # 初始化读取数据集
    def __len__(self):
        # 获取数据集的总大小
    def __getitem__(self, id):
        # 取回id所指定的数据
        
# 实例化上述类 进行数据迭代
dataset = my_data('my image path', 'my annotation path')

for data in dataset:
    print(data)

2、数据变换与数据增强

Pytorch提供了torchvision.transforms工具包,可以方便进行图像缩放、裁剪、随机翻转等一系列操作,如需要进行多个变换,可以利用transforms.Compose将多个变换整合起来,通常集成到Dataset的继承类中;

from torchvision import transforms

# 将transforms集成到Dataset类中, 
# 使用Compose将多个变换整合到一起

dataset = my_data(
    'my image path',
    'my annotation path',
    transform=transforms.Compose([
        transforms.Resize(256), # 将图像最短边缩小到256,宽高比例不变
        transforms.RandomHorizontalFlip(),  # 以0.5的概率随机反转指定图像
        transforms.ToTensor()
    ])
)

3、继承dataloader

通过torch.utils.data.Dataloader类进一步封装,可以实现对数据的批量处理,随机选取等操作;

from torch.utils.data import Dataloader

# 使用Dataloader进一步封装Dataset
# new_workers:表示用几个线程来加载数据
dataloader = Dataloader(dataset, batch_size=4, shuffle=True, num_workers=2)

# dataloader 是一个可迭代对象,对该实例迭代即可批量生成数据用于模型训练

data_iter = iter(dataloader)
for step in range(iters_per_epoch):
    data_batch = next(data_iter)
    # data_batch 用于网络训练

注意:在SSD中还对数据进行了一系列的处理,详情参考这篇文章:文章

二、SSD网络结构

1、骨干网络层

在这里插入图片描述

SSD采用的是VGG16作为骨干网络,采用了VGG16的前13个卷积层,然后用Conv 6和Conv7替代了原来的FC层;

  • Conv6中使用了空洞数为6的空洞卷积,其padding也为6,增加感受野的同时保持参数量和特征图尺寸不变;
pool5 =  nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)

2、深层卷积层

在这里插入图片描述

在VGG16的基础上,SSD增加了4个深度卷积网络层,用于提取更高层的语义信息,Conv8的通道数为512,Conv9、Conv10与Conv11的通道数都为256。从Conv7到Conv11,这5个卷积后输出特征图的尺寸依次为19×19、10×10、5×5、3×3和1×1;

  • 为了降低参数量,网络先用1×1卷积降低通道数再使用3×3卷积提取特征;

三、Prior Box的生成和匹配

1、Default Boxes和PriorBox

在这里插入图片描述

  • 如上图所示,Conv7、Conv8、Conv9、Conv10与Conv11的输出加上Conv4的输出,将作为用于检测的特征图;

  • 在SSD的设置中,每个Default Boxes(一个预测结果)中包含类别c + 位置信息(4位);像VOC数据集有20类,但SSD需要在前面添加一类背景类,也就是21类;

  • 对于每一层特征图而言,其中每一个单元不只预测一个Default Box,而是多个;这些预测的边界框是以先验框priorBox为基准的,在一定程度上减少训练难度;

在这里插入图片描述

  • PriorBox(anchor box)本质是在原图上的一系列矩形框,SSD使用了第4、7、8、9、10和11这六个卷积层的特征图,这6个特征图越来越小,对应的感受野越来越大;每个点分别对应4、6、6、6、 4、4个priorBox;
  • SSD300一共可以预测8732个边界框,是一个相当庞大的数字,SSD本质就是个密集采样;

2、Anchor(Prior Box)和Ground box的匹配

  • 将中心点坐标转化为角点坐标:(x,y,w,h -> x1,y1,x2,y2)
def point_form(boxes):
    return torch.cat((boxes[:, :2] - boxes[:, 2:]/2,     # xmin, ymin
                      boxes[:, :2] + boxes[:, 2:]/2), 1)  # xmax, ymax
def intersect(box_a, box_b):
    A = box_a.size(0)
    B = box_b.size(0)
    
    # 边框右下角的点
    max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(A, B, 2),
                       box_b[:, 2:].unsqueeze(0).expand(A, B, 2))
    # 边框左上角的点
    min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(A, B, 2),
                       box_b[:, :2].unsqueeze(0).expand(A, B, 2))
    
    inter = torch.clamp((max_xy - min_xy), min=0)
    return inter[:, :, 0] * inter[:, :, 1]
   
def jaccard(box_a, box_b):
    inter = intersect(box_a, box_b)  # 交集
    area_a = ((box_a[:, 2]-box_a[:, 0]) *
              (box_a[:, 3]-box_a[:, 1])).unsqueeze(1).expand_as(inter)  # [A,B]
    area_b = ((box_b[:, 2]-box_b[:, 0]) *
              (box_b[:, 3]-box_b[:, 1])).unsqueeze(0).expand_as(inter)  # [A,B]
    union = area_a + area_b - inter  # 并集
    return inter / union  # [A,B] # 交并比
  • 先验框编码问题

在这里插入图片描述

如上图所示,转换过程如上;

四、损失计算

1、整体网络架构

在这里插入图片描述

下面代码讲解损失部分前向推理的过程:

def forward(self, predictions, targets):
    loc_data, conf_data, priors = predictions  # 网络的输出值

    num = loc_data.size(0)  # num = batch_size
    priors = priors[:loc_data.size(1), :] # loc_data.size(1) = 8732, 因此 priors 维持不变
    num_priors = (priors.size(0))  # prior box 的数量, anchor的数量 num_priors= 8732
    num_classes = self.num_classes  # voc 数据集分21类
    
    # 1、首先匹配正负样本 匹配prior box (anchor)和 gt box
    # 先定义一个批次图像的数据容器大小 用于存放匹配后的样本数据 (定位数据loc_t和分类数据conf_t)
    loc_t = torch.Tensor(num, num_priors, 4)  # 定位 (batch_size, 8732,4)
    conf_t = torch.LongTensor(num, num_priors)  # 分类 (batch_size, 8732)

    # 处理一个批次中的每张图片
    for idx in range(num):
        # PyTorch0.4中,.data 仍保留,但建议使用 .detach(), 
        # 区别在于 .data 返回和 x 的相同数据 tensor, 但不会加入到x的计算历史里
        truths = targets[idx][:, :-1].data  
        labels = targets[idx][:, -1].data
        defaults = priors.data  
        # 得到每一个prior对应的truth,放到loc_t与conf_t中,conf_t中是类别,loc_t中是[matches, prior, variance]
        # 做好每一张图片中gt boxes和prior boxes的匹配
        match(self.threshold, truths, defaults, self.variance, labels,loc_t, conf_t, idx)

    if self.use_gpu:  # 如果有gpu的话将数据转存到gpu上加速计算
        loc_t = loc_t.cuda()
        conf_t = conf_t.cuda()
        
    # 2、计算所有正样本的定位损失,负样本不需要定位损失
    # 计算正样本的数量
    pos = conf_t > 0
    num_pos = pos.sum(dim=1, keepdim=True)

    # Localization Loss (Smooth L1)
    # Shape: [batch,num_priors,4]
    # 将pos_idx扩展为[32, 8732, 4],正样本的索引
    pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
    # 正样本的定位预测值 来源于模型的输出
    loc_p = loc_data[pos_idx].view(-1, 4)
    # 正样本的定位真值 来源于anchor_box与gt_box的match方法
    loc_t = loc_t[pos_idx].view(-1, 4)
    # 所有正样本的定位损失
    loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)
        
        
    # 3、对于类别损失,进行难例样本挖掘,控制比例为1:3
    # 所有prior的类别预测
    batch_conf = conf_data.view(-1, self.num_classes)
    # 计算类别损失.每一个的log(sum(exp(21个的预测)))-对应的真正预测值
    loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))

    # Hard Negative Mining
    loss_c = loss_c.view(pos.size()[0], pos.size()[1])
    # 首先过滤掉正样本
    loss_c[pos] = 0  # filter out pos boxes for now
    loss_c = loss_c.view(num, -1)
    _, loss_idx = loss_c.sort(1, descending=True)

    _, idx_rank = loss_idx.sort(1)
    num_pos = pos.long().sum(1, keepdim=True)

    # 这个地方负样本的最大值不应该是pos.size(1)-num_pos?
    num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
    # 选择每个batch中负样本的索引
    neg = idx_rank < num_neg.expand_as(idx_rank)
    
    # 4、计算正负样本的类别损失
    pos_idx = pos.unsqueeze(2).expand_as(conf_data)
    neg_idx = neg.unsqueeze(2).expand_as(conf_data)
    # 把预测提出来
    conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
    # 对应的标签
    targets_weighted = conf_t[(pos+neg).gt(0)]
    loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)

    # Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N

    N = num_pos.data.sum()  # 正样本个数
    loss_l /= N.type('torch.cuda.FloatTensor')
    loss_c /= N.type('torch.cuda.FloatTensor')
    return loss_l, loss_c

总结

代码部分可能讲的不是特别完善,这里推荐一篇博主的文章,写的很清楚明了:SSD核心代码详解

今天的文章二、SSD网络原理及代码讲解分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/32087.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注