二、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
细节部分:
- 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
-
IOU的计算
可参考文章:IOU的计算说明
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