SSD网络及代码理解

写在前面:在读了SSD的论文之后,完整的看了一遍SSD的代码,有了许多体会,以此记录自己的学习过程。
论文传送门:SSD: Single Shot MultiBox Detector
大佬复现的代码:link

一、SSD做了什么

Faster-rcnn有什么缺点:
1、由于是two-stage的网络,会先生成一些proposal,因此速度相对来说还是比较慢。而SSD直接作预测,可以达到50-60FPS的检测速度,达到了实时要求。
2、对小目标检测效果比较差。SSD在多尺度特征上做训练,因此不管是大目标,或者是小目标都有着比较好的检测效果。
图片来源此up主:link(up主的理论和代码讲解非常详细,给予了我很大帮助)

1、SSD结构图如下:

在这里插入图片描述
主干网络采用的VGG16:
在这里插入图片描述深一些的特征图感受野比较大,因此用来预测大目标,浅一些的特征图感受野比较小,用来预测小目标。关于感受野的一些年内容可以阅读此博客:VGG网络和感受野的理解.
在这里插入图片描述

抽出不同特征图来做回归和预测。

2、Default Box的选择

论文中讲解如下:
在这里插入图片描述
直观来看就是下图中所示:
在这里插入图片描述
一共会在6个特征图层上生成8732个Default Box。
在这里插入图片描述映射回原图

3、预测过程

在这里插入图片描述
和faster-rcnn不同的是,这里边界框回归参数预测是4k,而不是4num_classes*k

4、正负样本的选择

在仔细阅读了源代码后,先依据IOU值给每个Default Box分配对应的GT Box,然后里面IOU大于0.5的设置为正样本,同时,与GT Box最匹配的Default Box也设置为正样本(将正样本充分利用起来)。
负样本数是正样本数量的3倍,先将剩余的Default Box依据置信度排序,取排名靠前的对应数量的Default Box设置为负样本即可。

5、损失计算

和Faster-rcnn一样,类别损失利用正负样本,回归损失利用正样本即可。
具体可参考博客:link.

二、代码部分

大佬复现时,Backbone采用的时Resnet50
代码框架如下:
在这里插入图片描述

代码相对比较简单,看的过程中主要有两个部分有点疑惑

1、target部分

在读到损失计算部分时,这里target传入的数据比较疑惑
target_box(这里第一个4为batch_size数)
在这里插入图片描述
target_label

在这里插入图片描述
原来是在transform部分对target进行了操作,给出注释如下:

import random
import torchvision.transforms as t
from torchvision.transforms import functional as F
from src.utils import dboxes300_coco, calc_iou_tensor, Encoder
import torch

class Compose(object):
    def __init__(self, transforms):
        self.transforms = transforms

    def __call__(self, image, target=None):
        for trans in self.transforms:
            image, target = trans(image, target)
        return image, target

# 有些tensor并不是占用一整块内存,而是由不同的数据块组成,
# 而tensor的view()操作依赖于内存是整块的,这时只需要执行contiguous()这个函数,把tensor变成在内存中连续分布的形式。
class ToTensor(object):
    def __call__(self, image, target):
        image = F.to_tensor(image).contiguous()
        return image, target

class RandomHorizontalFlip(object):
    def __init__(self, prob=0.5):
        self.prob = prob

    def __call__(self, image, target):
        if random.random() < self.prob:
            # 水平翻转图片
            image = image.flip(-1)
            bbox = target["boxes"]
            # 水平翻转boxes信息
            bbox[:, [0, 2]] = 1.0 - bbox[:, [2, 0]]
            target["boxes"] = bbox
        return image, target

class SSDCropping(object):
    # 对图像进行裁减,该方法放在ToTensor之前
    def __init__(self):
        self.sample_options = (
            None,
            (0.1, None),
            (0.3, None),
            (0.5, None),
            (0.7, None),
            (0.9, None),
            (None, None),
        )
        # 8732*4,且都是相对坐标,想转化为绝对坐标,只用乘以原图大小即可
        # 一次传入batch_size张图片
        self.dboxes = dboxes300_coco()

    def __call__(self, image, target):
        while True:
            mode = random.choice(self.sample_options)
            if mode is None:
                return image, target
            # return跳出def函数

            # 这里的高宽是图像大小
            htot, wtot = target['height_width']

            min_iou, max_iou = mode
            min_iou = float('-inf') if min_iou is None else min_iou
            max_iou = float('+inf') if max_iou is None else max_iou

            for _ in range(5):
                w = random.uniform(0.3, 1.0)
                h = random.uniform(0.3, 1.0)

                # 如果一致,则跳过这次循环
                if w/h < 0.5 or w/h > 2:
                    continue

                # left 0 ~ wtot - w, top 0 ~ htot - h
                left = random.uniform(0, 1.0 - w)
                top = random.uniform(0, 1.0 - h)

                right = left + w
                bottom = top + h

                # boxes的坐标是在0-1之间的
                # 应该是裁减出有效目标
                bboxes = target["boxes"]
                ious = calc_iou_tensor(bboxes, torch.tensor([[left, top, right, bottom]]))

                if not ((ious > min_iou) & (ious < max_iou)).all():
                    continue

                # 这是算出中心坐标
                xc = 0.5 * (bboxes[:, 0] + bboxes[:, 2])
                yc = 0.5 * (bboxes[:, 1] + bboxes[:, 3])

                masks = (xc > left) & (xc < right) & (yc > top) & (yc < bottom)

                if not masks.any():
                    continue

                bboxes[bboxes[:, 0] < left, 0] = left
                bboxes[bboxes[:, 1] < top, 1] = top
                bboxes[bboxes[:, 2] < right, 2] = right
                bboxes[bboxes[:, 3] < bottom, 3] = bottom

                # 去除掉中心不在采样范围的GT Box
                bboxes = bboxes[masks, :]

                # 取出GT Box的标签
                labels = target['labels']
                labels = labels[masks]

                # 计算裁减之后的图像大小
                left_idx = int(left * wtot)
                top_idx = int(top * htot)
                right_idx = int(right * wtot)
                bottom_idx = int(bottom * htot)
                image = image.crop((left_idx, top_idx, right_idx, bottom_idx))

                # 调整之后的bboxes坐标信息,也是在0-1之间
                bboxes[:, 0] = (bboxes[:, 0] - left) / w
                bboxes[:, 1] = (bboxes[:, 1] - top) / h
                bboxes[:, 2] = (bboxes[:, 2] - left) / w
                bboxes[:, 3] = (bboxes[:, 3] - top) / h

                # 更新crop之后的GT Box坐标信息和标签信息
                target['boxes'] = bboxes
                target['labels'] = labels

                return image, target

class Resize(object):
    def __init__(self, size=(300, 300)):
        self.resize = t.Resize(size)

    def __call__(self, image, target):
        image = self.resize(image)
        return image, target

class ColorJitter(object):
    """对图像颜色进行随机调整,该方法应该放在ToTensor之前"""
    def __init__(self, brightness=0.125, contrast=0.5, saturation=0.5, hue=0.05):
        self.trans = t.ColorJitter(brightness, contrast, saturation, hue)

    def __call__(self, image, target):
        image = self.tarns(image)
        return image, target

# 对图像标准化的好处
# 1、提升模型的收敛速度
# 2、提高精度
# 3、防止梯度爆炸
class Normalization(object):
    def __init__(self, mean=None, std=None):
        if mean is None:
            mean = [0.485, 0.456, 0.406]
        if std is None:
            std = [0.229, 0.224, 0.225]
        self.normalize = t.Normalize(mean=mean, std=std)

    def __call__(self, image, target):
        image = self.normalize(image)
        return image, target

class AssignGTtoDefaultBox(object):
    def __init__(self):
        self.default_box = dboxes300_coco()
        self.encoder = Encoder(self.default_box)

    # 记录一下单引号和双引号没有区别,不过可以交互使用
    def __call__(self, image, target):
        boxes = target['boxes']
        labels = target["labels"]
        bboxes_out, labels_out = self.encoder.encode(boxes, labels)
        target['boxes'] = bboxes_out
        target['labels'] = labels_out

        return image, target

2、读取pth文件的一些理解

    backbone = Backbone()
    model = SSD300(backbone=backbone, num_classes=num_classes)
    # model = nn.DataParallel(model)
    pre_ssd_path = "./src/nvidia_ssdpyt_fp32.pt"
    if os.path.exists(pre_ssd_path) is False:
        raise FileNotFoundError("nvidia_ssdpyt_fp32.pt not find in {}".format(pre_ssd_path))
    pre_model_dict = torch.load(pre_ssd_path, map_location=device)
    pre_weights_dict = pre_model_dict["model"]

    # 删除类别预测器权重,注意,回归预测器的权重可以重用,因为不涉及num_classes
    del_conf_loc_dict = {}
    for k, v in pre_weights_dict.items():
        split_key = k.split(".")
        if "conf" in split_key:
            continue
        del_conf_loc_dict.update({k: v})

    missing_keys, unexpected_keys = model.load_state_dict(del_conf_loc_dict, strict=False)
    # torch.save(model.state_dict(), './aaaaaaaaa.pth')
    # a = torch.load('./aaaaaaaaa.pth')
    # c, d = model.load_state_dict(a, strict=False)

由于加载的权重之前训练的类别数和我们现在要预测的类别数不一样,因此我们需要删除掉分类权部分重。
如上图,对于missing_keys, unexpected_keys参数不太理解,调试后发现missing_keys是model里面有的参数,del_conf_loc_dict里面没有的参数。unexpected_keys是del_conf_loc_dict里面有的,model里面没有的参数。
在这里插入图片描述
也就是说有14层参数model里有,而del_conf_loc_dict里没有
在这里插入图片描述
del_conf_loc_dict里有的,model里也都有

验证如下,我们先保存model的参数文件,再调用上面那个函数,理论上来说c和d都应该是空列表

    backbone = Backbone()
    model = SSD300(backbone=backbone, num_classes=num_classes)
    # model = nn.DataParallel(model)
    pre_ssd_path = "./src/nvidia_ssdpyt_fp32.pt"
    if os.path.exists(pre_ssd_path) is False:
        raise FileNotFoundError("nvidia_ssdpyt_fp32.pt not find in {}".format(pre_ssd_path))
    pre_model_dict = torch.load(pre_ssd_path, map_location=device)
    pre_weights_dict = pre_model_dict["model"]

    # 删除类别预测器权重,注意,回归预测器的权重可以重用,因为不涉及num_classes
    del_conf_loc_dict = {}
    for k, v in pre_weights_dict.items():
        split_key = k.split(".")
        if "conf" in split_key:
            continue
        del_conf_loc_dict.update({k: v})

    missing_keys, unexpected_keys = model.load_state_dict(del_conf_loc_dict, strict=False)
    torch.save(model.state_dict(), './aaaaaaaaa.pth')
    a = torch.load('./aaaaaaaaa.pth')
    c, d = model.load_state_dict(a, strict=False)

调试结果如下:
在这里插入图片描述
同时要保存或调用整个模型文件,使用函数:

#保存模型
torch.save(model_object,'resnet.pth')
#加载模型
model=torch.load('resnet.pth')

如果只用保存模型参数或则加载模型参数可用使用如下函数:

#将my_resnet模型存储为my_resnet.pth
torch.save(my_resnet.state_dict(),"my_resnet.pth")
#加载resnet,模型存放在my_resnet.pth
my_resnet.load_state_dict(torch.load("my_resnet.pth"))
展开阅读全文
  • 4
    点赞
  • 3
    评论
  • 5
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页

打赏

哪来那么多热情^^

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值