【项目一、xxx病虫害检测项目】1、SSD原理和源码分析

前言

马上要找工作了,想总结下自己做过的几个小项目。

先总结下实验室之前的一个病虫害检测相关的项目。选用的baseline是SSD,代码是在这个仓库的基础上改的 lufficc/SSD.这个仓库写的ssd还是很牛的,github有1.3k个star。

选择这个版本的代码,主要有两个原因:

  1. 它的backbone代码是支持直接加载pytorch官方预训练权重的,所以很方便我做实验
  2. 代码高度模块化,类似mmdetection和Detectron2,写的很高级,不过对初学者不是很友好,但是很能提高工程代码能力。

原仓库主要实现了SSD-VGG16、SSD-Mobilenet-V2、SSD-Mobilenet-V3、SSD-EfficientNet等网络,在我数据集上几个改进版本都还不如SSD-VGG16效果好,所以我在原仓库的基础上进行了自己的实验,加了一些也不算很高级的trick吧,主要是在我的数据集上确实好使,疯狂调参,哈哈哈。

后续代码也会上传到github上的。

同系列讲解:
【项目一、xxx病虫害检测项目】2、网络结构尝试改进:Resnet50、SE、CBAM、Feature Fusion.
【项目一、xxx病虫害检测项目】3、损失函数尝试:Focal loss.

第一篇,先介绍下SSD的原理和源码吧。

代码已全部上传GitHub: HuKai-cv/FFSSD-ResNet..

一、SSD backbone

1.1、总体结构

总体结构:
在这里插入图片描述
backbone主要由两个部分构成:第一个部分是原先的vgg结构(conv1_1->conv_53)和替换vgg的部分(pool5->conv7);第二个部分是一些额外的添加层结构:conv8_1->conv11_2。总共有6个预测特征层,分别是Conv4_3、Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2,前面两个是backbone生成的,后面4个是额外添加层生成的。

对应代码在ssd/modeling/backbone/vgg.py中:

@registry.BACKBONES.register('vgg')
def vgg(cfg, pretrained=True):
    """搭建vgg模型+加载预训练权重
    Args:
        cfg: vgg配置文件
        pretrained: 是否加载预训练模型
    Returns:
        返回加载完预训练权重的vgg模型
    """
    model = VGG(cfg)  # 搭建vgg模型
    if pretrained:    # 加载预训练权重
        # 这里默认会从amazonaws平台下载vgg模型预训练权重文件
        model.init_from_pretrain(load_state_dict_from_url(model_urls['vgg']))
    return model
    
vgg_base = {
    '300': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
            512, 512, 512],
    '512': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',
            512, 512, 512],
}
extras_base = {
    '300': [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256],
    '512': [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256],
}

class VGG(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        size = cfg.INPUT.IMAGE_SIZE  # 输入图片大小
        # ssd-vgg16 backbone配置信息 [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512]
        vgg_config = vgg_base[str(size)]
        # vgg之后额外的一些特征提取层配置信息 [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256]
        extras_config = extras_base[str(size)]

        # 初始化backbone
        self.vgg = nn.ModuleList(add_vgg(vgg_config))
        # 初始化额外特征提取层
        self.extras = nn.ModuleList(add_extras(extras_config, i=1024, size=size))
        self.l2_norm = L2Norm(512, scale=20)
        self.reset_parameters()  # 参数初始化

    def reset_parameters(self):  # 参数初始化
        for m in self.extras.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_uniform_(m.weight)
                nn.init.zeros_(m.bias)

    def init_from_pretrain(self, state_dict):  # 载入backbone预训练权重
        self.vgg.load_state_dict(state_dict)

    def forward(self, x):
        features = []  # 存放6个预测特征层
        # 前23层conv1_1->conv4_3(包括relu层)前序传播
        for i in range(23):
            x = self.vgg[i](x)
        s = self.l2_norm(x)  # Conv4_3 L2 normalization  论文里这样说的 不知道为什么
        features.append(s)   # append conv4_3

        # apply vgg up to fc7
        for i in range(23, len(self.vgg)):
            x = self.vgg[i](x)
        features.append(x)  # append conv7

        for k, v in enumerate(self.extras):
            x = F.relu(v(x), inplace=True)
            if k % 2 == 1:
                features.append(x)  # append conv8_2 conv9_2 conv10_2 conv11_2

        # ssd300返回六个预测特征层  ssd512返回7个预测特征层
        return tuple(features)

1.2、修改vgg

模型结构的部分对应的是vgg中的Conv1_1->Conv5_3,如下图所示(图片来自b站霹雳吧啦Wz:2.1SSD算法理论.):
在这里插入图片描述
而Conv4_3得到的特征图就是我们的第一个预测特征图,Conv5_3之后接一个pool5,和原论文不同的是,这里将pool5从原来的2x2/2变为3x3/1,这样经过pool5之后特征层尺寸不变。再之和再接一个3x3的卷积Conv6和1x1卷积Conv7代替vgg16中的两个全连接层fc6、fc7,到此backbone-vgg部分就全部构建完成了。而且Conv7的输出特征层就算第二个预测特征图。

对比下(图片来自just_sort: 目标检测算法之SSD代码解析(万字长文超详细).):
在这里插入图片描述

对应代码在ssd/modeling/backbone/vgg.py中:

# borrowed from https://github.com/amdegroot/ssd.pytorch/blob/master/ssd.py
def add_vgg(cfg, batch_norm=False):
    """根据配置文件搭建SSD中的backbone(Conv1_1->Conv7)模型结构
    pool3x3/1替换vgg中的pool2x2/2  conv6(空洞卷积 r=6)、conv7替换fc6、fc7  其中conv4_3、conv7都得到预测特征图
    Args:
        cfg: Conv1_1->Conv7配置文件 [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512]
             数字表示当前卷积层的输出channel 'M'表示maxpooling采用向下取整的形式如9x9->4x4 'C'相反表示向上取整如9x9->5x5
        batch_norm: True conv+bn+relu
                    False conv relu  原论文是没有bn层的
    Returns: backbone层结构 卷积层+maxpool共17层
    """
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        elif v == 'C':
            # ceil mode feature map size是奇数时 最大池化下采样向上取整 如 9x9->5x5
            layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v  # 下一层的输入=这一层的输出
    # 论文里有说将vgg中的pool5的2x2/2变为3x3/1 这样这层的池化后特征图尺寸不变
    pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
    # conv6使用了空洞卷积 原论文也是这样说的
    conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)
    conv7 = nn.Conv2d(1024, 1024, kernel_size=1)
    layers += [pool5, conv6,
               nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]
    return layers

1.3、额外添加层

额外添加层主要由4个部分构成,Conv8:conv1x1x256 + conv3x3x512/2;Conv9:conv1x1x128 + conv3x3x256/2;Conv10:conv1x1x128 + conv3x3x256/1;Conv11:conv1x1x128 + conv3x3x256/1

对应代码在ssd/modeling/backbone/vgg.py中:

def add_extras(cfg, i, size=300):
    """
    backbone之后的4个额外添加层 生成4个预测特征图
    Extra layers added to VGG for feature scaling
    Args:
        cfg: 4个额外添加层的配置文件  [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256]
             数字代表当前卷积层输出channel当前卷积层s=1  'S'代表当前卷积层s=2
        i: 1024 第一个额外特征层 也就是conv8_1的输入channel
        size: 输入图片大小
    Returns: 4个额外添加层结构
    """
    layers = []
    in_channels = i
    flag = False
    for k, v in enumerate(cfg):
        if in_channels != 'S':
            if v == 'S':
                layers += [nn.Conv2d(in_channels, cfg[k + 1], kernel_size=(1, 3)[flag], stride=2, padding=1)]
            else:
                layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]
            flag = not flag
        in_channels = v
    if size == 512:
        layers.append(nn.Conv2d(in_channels, 128, kernel_size=1, stride=1))
        layers.append(nn.Conv2d(128, 256, kernel_size=4, stride=1, padding=1))
    return layers

好了整个网络结构介绍完了。

1.4、需要注意的点

  1. ssd整个网络没有bn
  2. ssd的conv4_3输出的特征图进行了L2 norm操作,因为网络层靠前,方差比较大,需要加一个L2标准化,以保证和后面的检测层差异不是很大。
  3. 3、conv6 用了3x3空洞卷积 p=6 r=6

二、SSD head

SSD head分为三部分,第一部分将6/7个预测特征层输入检测头得到最终的预测回归参数和预测分类结果;第二部分训练时根据最终的预测回归参数和预测分类结果和gt计算损失;第三部分测试时对预测分类结果进行softmax处理,对预测回归参数利用default box进行解码,nms等处理。

对应代码在ssd/modeling/box_head/box_head.py中:

@registry.BOX_HEADS.register('SSDBoxHead')
class SSDBoxHead(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.cfg = cfg
        self.predictor = make_box_predictor(cfg)  # 预测器 类型SSDBoxPredictor
        self.loss_evaluator = MultiBoxLoss(neg_pos_ratio=cfg.MODEL.NEG_POS_RATIO)  # 损失函数 类型MutiBoxLoss
        # self.loss_evaluator = Focal_loss(neg_pos_ratio=cfg.MODEL.NEG_POS_RATIO)
        self.post_processor = PostProcessor(cfg)  # 后处理
        self.priors = None  # default box

    def forward(self, features, targets=None):
        """
        Args:
            features: tuple7 [bs,512,64,64] [bs,1024,32,32] [bs,512,16,16] [bs,256,8,8] [bs,256,4,4] [bs,256,2,2] [bs,256,1,1]
            targets: 'boxes': [bs, 24564, 4]   'labels': [bs, 24564]
        Returns:

        """
        # cls_logits: [bs,all_anchors,num_classes]=[bs, 24564, 6]
        # bbox_pred: [bs,all_anchors,xywh]=[bs, 24564, 4]
        cls_logits, bbox_pred = self.predictor(features)  # 预测器前线传播
        if self.training:  # 训练返回预测结果和loss
            return self._forward_train(cls_logits, bbox_pred, targets)
        else:  # 测试和验证返回预测结果和{}
            return self._forward_test(cls_logits, bbox_pred)

    def _forward_train(self, cls_logits, bbox_pred, targets):
        """
        Args:
            cls_logits: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6]  0代表背景类 真实类别=num_classes-1
            bbox_pred: 回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]
            targets: gt 'boxes': [bs, 24564, 4]   'labels': [bs, 24564]
        Returns: 预测结果+loss
        """
        # gt_boxes: [bs, 24564, 4]   gt_labels: [bs, 24564]
        gt_boxes, gt_labels = targets['boxes'], targets['labels']
        # 计算损失
        reg_loss, cls_loss = self.loss_evaluator(cls_logits, bbox_pred, gt_labels, gt_boxes)  # 回归损失 + 分类损失
        loss_dict = dict(
            reg_loss=reg_loss,
            cls_loss=cls_loss,
        )
        detections = (cls_logits, bbox_pred)
        return detections, loss_dict  # 返回预测结果和loss

    def _forward_test(self, cls_logits, bbox_pred):
        """
        Args:
            cls_logits: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6]
            bbox_pred: 回归预测结果  [bs,all_anchors,xywh]=[bs, 24564, 4]
        Returns:
        """
        if self.priors is None:
            self.priors = PriorBox(self.cfg)().to(bbox_pred.device)
        scores = F.softmax(cls_logits, dim=2)  # 分类结果在classes_num维度进行softmax [bs,24564,num_classes]
        # 利用default box将预测回归参数进行解码  [bs, num_anchors, xywh(归一化后的真实的预测坐标)]
        boxes = box_utils.convert_locations_to_boxes(
            bbox_pred, self.priors, self.cfg.MODEL.CENTER_VARIANCE, self.cfg.MODEL.SIZE_VARIANCE
        )
        # xywh->xyxy  将归一化后的预测坐标由xywh->xyxy
        boxes = box_utils.center_form_to_corner_form(boxes)
        detections = (scores, boxes)
        detections = self.post_processor(detections)  # 后处理
        return detections, {}

2.1、检测头predictor

第一部分网络结构总共得到6个特征图,分别是Conv4_3,Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2的输出特征图。接下来就是就这些预测特征层送入检测头中了,其实就是一个3x3个Conv直接对default box进行分类预测和回归预测。

base检测头对应代码在ssd/modeling/box_head/box_predictor.py中:

class BoxPredictor(nn.Module):  # base box predictor
    def __init__(self, cfg):
        super().__init__()
        self.cfg = cfg  # predictor配置文件
        self.cls_headers = nn.ModuleList()   # 7个预测特征层对应的7个分类头 全是3x3conv s=1 p=1
        self.reg_headers = nn.ModuleList()   # 7个预测特征层对应的7个回归头 全是3x3cong s=1 p=1
        # 每层default box个数 [4, 6, 6, 6, 6, 4, 4]
        # 7个预测特征层输出channel数=7个分类头/回归头的输入channel (512, 1024, 512, 256, 256, 256, 256)
        for level, (boxes_per_location, out_channels) in enumerate(zip(cfg.MODEL.PRIORS.BOXES_PER_LOCATION, cfg.MODEL.BACKBONE.OUT_CHANNELS)):
            # conv3x3 s=1 p=1 in_channel=(512, 1024, 512, 256, 256, 256, 256) out_channel=[4, 6, 6, 6, 6, 4, 4]*num_classes
            self.cls_headers.append(self.cls_block(level, out_channels, boxes_per_location))
            # conv3x3 s=1 p=1 in_channel=(512, 1024, 512, 256, 256, 256, 256) out_channel=[4, 6, 6, 6, 6, 4, 4]*4
            self.reg_headers.append(self.reg_block(level, out_channels, boxes_per_location))
        self.reset_parameters()

    def cls_block(self, level, out_channels, boxes_per_location):
        raise NotImplementedError

    def reg_block(self, level, out_channels, boxes_per_location):
        raise NotImplementedError

    def reset_parameters(self):  # 初始化权重
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.xavier_uniform_(m.weight)
                nn.init.zeros_(m.bias)

    def forward(self, features):
        cls_logits = []  # 存放所有head分类预测结果
        bbox_pred = []   # 存放所有head回归预测结果
        for feature, cls_header, reg_header in zip(features, self.cls_headers, self.reg_headers):
            # [bs,h,w,num_classes*box_nums]  其中num_classes=6 box_nums=[4, 6, 6, 6, 6, 4, 4]
            # [bs,64,64,24] [bs,32,32,32] [bs,16,16,32] [bs,64,64,32] [bs,64,64,32] [bs,64,64,24] [bs,64,64,24]
            cls_logits.append(cls_header(feature).permute(0, 2, 3, 1).contiguous())
            # [bs,h,w,num_classes*4] 其中num_classes=6 4=xywh
            # [bs,64,64,16] [bs,32,32,24] [bs,16,16,24] [bs,64,64,24] [bs,64,64,24] [bs,64,64,16] [bs,64,64,16]
            bbox_pred.append(reg_header(feature).permute(0, 2, 3, 1).contiguous())

        batch_size = features[0].shape[0]
        # [bs,all_anchors,num_classes]=[2,24564,6]
        cls_logits = torch.cat([c.view(c.shape[0], -1) for c in cls_logits], dim=1).view(batch_size, -1, self.cfg.MODEL.NUM_CLASSES)
        # [bs,all_anchors,xywh]=[bs,24564,4]
        bbox_pred = torch.cat([l.view(l.shape[0], -1) for l in bbox_pred], dim=1).view(batch_size, -1, 4)

        return cls_logits, bbox_pred

可以看到默认是没有检测头的,但是SSD继承base head的同时重写了检测头对应代码在ssd/modeling/box_head/box_predictor.py中::

@registry.BOX_PREDICTORS.register('SSDBoxPredictor')
class SSDBoxPredictor(BoxPredictor):
    # 继承自base box predict BoxPredictor
    # 重写分类预测头
    def cls_block(self, level, out_channels, boxes_per_location):
        return nn.Conv2d(out_channels, boxes_per_location * self.cfg.MODEL.NUM_CLASSES, kernel_size=3, stride=1, padding=1)
    # 重写回归预测头
    def reg_block(self, level, out_channels, boxes_per_location):
        return nn.Conv2d(out_channels, boxes_per_location * 4, kernel_size=3, stride=1, padding=1)

2.2、生成default box

论文生成过程:
Default box的设置包含尺度min_size、max_size和长宽比两个方面。
尺度公式如下:
在这里插入图片描述

m代表特征层层数,这里是5,因为第一层单独设置。s_k表示anchor尺寸相对于原图的比例,s_min和s_max表示比例的最小值和最大值,论文中分别为0.2和0.9。第一层单独设置比例为s_min/2=0.1,所以它的尺度为300x0.1=30,后面的特征图按照线性公式计算,且下一层的min_size=这一层的max_size。最后可以得到每个特征图的min_size和max_size。
在这里插入图片描述

再为每一层人为设置了长宽比;
根据每层的min_size和max_size和长宽比生成各层的default box(1个min_size的1:1;1个min_size*max_size的1:1;2n)。

但是实际代码中default box的尺度和长宽比都是人为设置的。

对应代码在ssd/modeling/anchors/prior_box.py中:

class PriorBox:
    def __init__(self, cfg):
        # 这里的min_size和max_size是直接给出的  论文是公式求出来的
        self.image_size = cfg.INPUT.IMAGE_SIZE  # 512 图片大小
        prior_config = cfg.MODEL.PRIORS
        self.feature_maps = prior_config.FEATURE_MAPS  # 所有层的featue map size [64, 32, 16, 8, 4, 2, 1]
        self.min_sizes = prior_config.MIN_SIZES        # 所有层的min_size[35.84, 76.8, 153.6, 230.4, 307.2, 384.0, 460.8]
        self.max_sizes = prior_config.MAX_SIZES        # 所有层的max_size[76.8, 153.6, 230.4, 307.2, 384.0, 460.8, 537.65]
        self.strides = prior_config.STRIDES            # 所有层的stride[8, 16, 32, 64, 128, 256, 512]
        self.aspect_ratios = prior_config.ASPECT_RATIOS  # 所有层的aspect ratio[[2], [2, 3], [2, 3], [2, 3], [2, 3], [2], [2]]
        self.clip = prior_config.CLIP  # True

    def __call__(self):
        """Generate SSD Prior Boxes.
            It returns the center, height and width of the priors. The values are relative to the image size
            Returns:
                priors (num_priors, 4): The prior boxes represented as [[center_x, center_y, w, h]]. All the values
                    are relative to the image size.
        """
        priors = []
        for k, f in enumerate(self.feature_maps):
            scale = self.image_size / self.strides[k]
            for i, j in product(range(f), repeat=2):
                # unit center x,y   default box中心点坐标(相对特征图)
                cx = (j + 0.5) / scale
                cy = (i + 0.5) / scale

                # 生成default box
                # small sized square box  w/h=1:1
                size = self.min_sizes[k]
                h = w = size / self.image_size
                priors.append([cx, cy, w, h])

                # big sized square box  w/h=1:1
                size = sqrt(self.min_sizes[k] * self.max_sizes[k])
                h = w = size / self.image_size
                priors.append([cx, cy, w, h])

                # change h/w ratio of the small sized box  w/h=ratio and 1/ratio
                size = self.min_sizes[k]
                h = w = size / self.image_size
                for ratio in self.aspect_ratios[k]:
                    ratio = sqrt(ratio)
                    priors.append([cx, cy, w * ratio, h / ratio])
                    priors.append([cx, cy, w / ratio, h * ratio])

        priors = torch.tensor(priors)
        if self.clip:
            priors.clamp_(max=1, min=0)
        return priors

2.3、计算分类回归损失

损失分为两个部分分类损失+回归损失,分类损失针对正样本+负样本(困难样本挖掘负样本:正样本=3:1)用的是softmax+cross entropy。在ssd和faster rcnn中分类损失其实也包含了置信度(预测框包含物体的概率)损失,因为ssd分类通常会将背景也当作一个类别,最后输出接softmax得到20个类别分数和1个置信度分数。;回归损失针对所有正样本使用smooth l1损失函数。

对应代码在ssd/modeling/box_head/loss.py中:

class MultiBoxLoss(nn.Module):
    def __init__(self, neg_pos_ratio):
        """Implement SSD MultiBox Loss.

        Basically, MultiBox loss combines classification loss
         and Smooth L1 regression loss.
        """
        super(MultiBoxLoss, self).__init__()
        self.neg_pos_ratio = neg_pos_ratio  # 负样本/正样本=3:1

    def forward(self, confidence, predicted_locations, labels, gt_locations):
        """计算分类损失和回归损失
        Args:
            confidence: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6]  0代表背景类 真实类别=num_classes-1
            predicted_locations: 回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]
            labels: gt_labels 所有anchor的真实框类别标签 0表示背景类 [bs, 24564]
            gt_locations: 所有anchor的真实框回归标签 [bs, 24564, 4]
        Returns:
        """
        num_classes = confidence.size(2)  # 6
        with torch.no_grad():  # 困难样本挖掘
            # derived from cross_entropy=sum(log(p))
            # log_softmax(confidence, dim=2): [bs,24564,6] 对confidence的num_classes维度进行softmax出来再log
            # -F.log_softmax(confidence, dim=2)[:, :, 0]: [bs,24564] 所有anchor属于背景的损失
            loss = -F.log_softmax(confidence, dim=2)[:, :, 0]  # [bs,anchor_nums]
            mask = box_utils.hard_negative_mining(loss, labels, self.neg_pos_ratio)

        confidence = confidence[mask, :]  # [104,6] 正样本[1,25] 负样本[3,75]  正样本+负样本=1+25 + 3+75 = 104
        # 分类损失 sum  正样本:负样本=3:1  预测输入: [pos+neg, num_classes]  gt输入: [pos+neg]
        # F.cross_entropy: 1、对预测输入进行softmax+log运算 -> [pos+neg, num_classes]
        #                  2、对gt输入进行one-hot编码 [pos+neg] -> [pos+neg, num_classes]
        #                  3、再对预测和gt进行交叉熵损失计算
        classification_loss = F.cross_entropy(confidence.view(-1, num_classes), labels[mask], reduction='sum')

        pos_mask = labels > 0
        predicted_locations = predicted_locations[pos_mask, :].view(-1, 4)  # 正样本预测回归参数 [26,4]
        gt_locations = gt_locations[pos_mask, :].view(-1, 4)  # 正样本gt label [26,4]
        # 回归loss smooth l1 sum(只计算正样本)
        smooth_l1_loss = F.smooth_l1_loss(predicted_locations, gt_locations, reduction='sum')
        num_pos = gt_locations.size(0)  # 正样本个数 26
        # 回归总损失(正样本)/正样本个数   分类总损失(正样本+负样本)/正样本个数
        return smooth_l1_loss / num_pos, classification_loss / num_pos

2.3.1、正负样本匹配:MaxIOUAssign

正负样本匹配规则:

  1. 正样本:对每个gt找到和它iou最大的default box,作为这个gt的正样本,这样可以保证每个gt至少有一个正样本;一个default box和所有gt的最大IOU大于0.5时,这个default box为正样本;(1和2同时起作用,可能某个gt会匹配到多个正样本,但是一个default box只能是一个gt的正样本。)
  2. 负样本:一个default box和所有gt的最大IOU小于0.5时,该default box为负样本;
  3. 没有忽略样本

对应代码在ssd/utils/box_utils.py中:

def assign_priors(gt_boxes, gt_labels, corner_form_priors, iou_threshold):
    """把每个prior box进行正负样本匹配
    Assign ground truth boxes and targets to priors.
    Args:
        gt_boxes (num_targets, 4): ground truth boxes. 图片的所有类别
        gt_labels (num_targets): labels of targets. gt图片的所有类别
        priors (num_priors, 4): corner form priors  先验框 default box xyxy
        iou_threshold: 0.5   IOU阈值,小于阈值设为背景
    Returns:
        boxes (num_priors, 4): real values for priors.
        labels (num_priros): labels for priors.
    """
    # size: num_priors x num_targets  gt和default box的iou
    ious = iou_of(gt_boxes.unsqueeze(0), corner_form_priors.unsqueeze(1))
    # size: num_priors  和每个ground truth box 交集最大的 prior box
    best_target_per_prior, best_target_per_prior_index = ious.max(1)
    # size: num_targets  和每个prior box 交集最大的 ground truth box
    best_prior_per_target, best_prior_per_target_index = ious.max(0)

    # 保证每一个ground truth 匹配它的都是具有最大IOU的prior
    # 根据 best_prior_dix 锁定 best_truth_idx里面的最大IOU prior
    for target_index, prior_index in enumerate(best_prior_per_target_index):
        best_target_per_prior_index[prior_index] = target_index
    # 2.0 is used to make sure every target has a prior assigned
    # 保证每个ground truth box 与某一个prior box 匹配,固定值为 2 > threshold
    best_target_per_prior.index_fill_(0, best_prior_per_target_index, 2)
    # size: num_priors  提取出所有匹配的ground truth box
    labels = gt_labels[best_target_per_prior_index]
    # 把 iou < threshold 的框类别设置为 bg,即为0
    labels[best_target_per_prior < iou_threshold] = 0  # the backgournd id
    # 匹配好boxes
    boxes = gt_boxes[best_target_per_prior_index]
    return boxes, labels

2.3.2、平衡正负样本:在线困难样本挖掘

困难样本挖掘主要是为了针对负样本太多的情况,如果全部负样本都参与计算损失,那么一定会造成正负样本的比例失衡,所以这里主要计算一些较为困难的负样本的损失,即loss较大的负样本。

具体做法:将所有负样本的分类损失按降序排序,取前正样本3个负样本作为最终的分类负样本,参与分类损失计算。
正样本个数=所有正样本个数 负样本个数=正样本个数
3

对应代码在ssd/utils/box_utils.py中:

def hard_negative_mining(loss, labels, neg_pos_ratio):
    """
    It used to suppress the presence of a large number of negative prediction.
    It works on image level not batch level.
    For any example/image, it keeps all the positive predictions and
     cut the number of negative predictions to make sure the ratio
     between the negative examples and positive examples is no more
     the given ratio for an image.

    Args:
        loss (N, num_priors): the loss for each example.  [bs,24564] 所有anchor属于背景的损失
        labels (N, num_priors): the labels. 所有anchor的真实框类别标签 0表示背景类 [bs, 24564]
        neg_pos_ratio:  the ratio between the negative examples and positive examples. 负样本:正样本=3:1
    """
    pos_mask = labels > 0  # 正类True 负类背景类False
    num_pos = pos_mask.long().sum(dim=1, keepdim=True)   # [1个,25个] 第一张图1个正样本 第二张图25个正样本
    num_neg = num_pos * neg_pos_ratio  # [3, 75] 第一张图3个负样本 第二张图75个负样本

    loss[pos_mask] = -math.inf
    _, indexes = loss.sort(dim=1, descending=True)  # 对负样本损失进行排序
    _, orders = indexes.sort(dim=1)
    neg_mask = orders < num_neg  # 选出损失最高的前num_neg个负样本
    return pos_mask | neg_mask

2.5、后处理

后处理主要是针对预测得到的回归参数,利用上一步生成的default box进行解码,得到所有anchor的预测结果,再对这些预测结果进行nms等处理,得到最终的预测结果。

2.5.1、回归参数编解码:

解码:利用default box,将边界框回归参数解码为真实坐标。一般用在验证测试时,将预测框解码为真实坐标,再进行nms,显示在原图上。公式:
在这里插入图片描述

对应代码在ssd/utils/box_utils.py中:

def convert_locations_to_boxes(locations, priors, center_variance,
                               size_variance):
    """Convert regressional location results of SSD into boxes in the form of (center_x, center_y, h, w).

    The conversion:
        $$predicted\_center * center_variance = \frac {real\_center - prior\_center} {prior\_hw}$$
        $$exp(predicted\_hw * size_variance) = \frac {real\_hw} {prior\_hw}$$
    We do it in the inverse direction here.
    Args:
        locations (batch_size, num_priors, 4): the regression output of SSD. It will contain the outputs as well.
                  回归预测结果  [bs,all_anchors,xywh]=[bs, 24564, 4]
        priors (num_priors, 4) or (batch_size/1, num_priors, 4): prior boxes.
               [all_anchor, 4]
        center_variance: a float used to change the scale of center.    0.1
        size_variance: a float used to change of scale of size.         0.2
    Returns:
        boxes:  priors: [[center_x, center_y, w, h]]. All the values
            are relative to the image size.
    """
    # priors can have one dimension less.
    if priors.dim() + 1 == locations.dim():
        priors = priors.unsqueeze(0)  # [num_anchors, 4] -> [1, num_anchors, 4]
    return torch.cat([
        # xy  利用default box将xy回归参数进行解码
        # 解码后xy坐标=预测的xy参数*default box的wh坐标*default box的xy坐标方差+default box的xy坐标
        locations[..., :2] * center_variance * priors[..., 2:] + priors[..., :2],
        # wh  利用default box将wh回归参数进行解码
        # 解码后的wh坐标=e^(预测的wh参数*default box的wh坐标方差) * default box的wh坐标
        torch.exp(locations[..., 2:] * size_variance) * priors[..., 2:]
    ], dim=locations.dim() - 1)

编码:利用default box,将真实坐标编码为边界框参数,一般用在训练时,对gt label编码,将gt的坐标编码为回归参数,再与预测得到的预测框回归参数一起计算损失。公式(和上面解码过程刚好相反):

在这里插入图片描述

对应代码在ssd/utils/box_utils.py中:

def convert_boxes_to_locations(center_form_boxes, center_form_priors, center_variance, size_variance):
    # priors can have one dimension less
    if center_form_priors.dim() + 1 == center_form_boxes.dim():
        center_form_priors = center_form_priors.unsqueeze(0)
    return torch.cat([
        (center_form_boxes[..., :2] - center_form_priors[..., :2]) / center_form_priors[..., 2:] / center_variance,
        torch.log(center_form_boxes[..., 2:] / center_form_priors[..., 2:]) / size_variance
    ], dim=center_form_boxes.dim() - 1)

2.5.2、NMS

对应代码在ssd/modeling/box_head/inference.py中:

class PostProcessor:
    def __init__(self, cfg):
        super().__init__()
        self.cfg = cfg
        self.width = cfg.INPUT.IMAGE_SIZE
        self.height = cfg.INPUT.IMAGE_SIZE

    def __call__(self, detections):
        batches_scores, batches_boxes = detections
        device = batches_scores.device
        batch_size = batches_scores.size(0)
        results = []
        for batch_id in range(batch_size):
            scores, boxes = batches_scores[batch_id], batches_boxes[batch_id]  # (N, #CLS) (N, 4)
            num_boxes = scores.shape[0]
            num_classes = scores.shape[1]

            boxes = boxes.view(num_boxes, 1, 4).expand(num_boxes, num_classes, 4)
            labels = torch.arange(num_classes, device=device)
            labels = labels.view(1, num_classes).expand_as(scores)

            # remove predictions with the background label
            boxes = boxes[:, 1:]
            scores = scores[:, 1:]
            labels = labels[:, 1:]

            # batch everything, by making every class prediction be a separate instance
            boxes = boxes.reshape(-1, 4)
            scores = scores.reshape(-1)
            labels = labels.reshape(-1)

            # remove low scoring boxes
            indices = torch.nonzero(scores > self.cfg.TEST.CONFIDENCE_THRESHOLD).squeeze(1)
            boxes, scores, labels = boxes[indices], scores[indices], labels[indices]

            boxes[:, 0::2] *= self.width
            boxes[:, 1::2] *= self.height

            # nms
            keep = batched_nms(boxes, scores, labels, self.cfg.TEST.NMS_THRESHOLD)
            
            # keep only topk scoring predictions
            keep = keep[:self.cfg.TEST.MAX_PER_IMAGE]
            boxes, scores, labels = boxes[keep], scores[keep], labels[keep]

            container = Container(boxes=boxes, labels=labels, scores=scores)
            container.img_width = self.width
            container.img_height = self.height
            results.append(container)
        return results

三、数据增强

SSD的效果好,还有一个很重要的原因:SSD做了丰富的数据增强策略,这部分为模型mAP带来了8.8%的提升,尤其是小物体和遮挡问题等难点问题,强大的数据增强很重要。

这部分代码在ssd/data/transforms/transformers.py中,自己看下就ok了

Reference

b站霹雳吧啦Wz:2.1SSD算法理论.

CSDN just_sort:目标检测算法之SSD代码解析(万字长文超详细).

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
【资源说明】 Java基于mybatis的数据库加密项目源码+项目使用说明.zip 该项目是个人毕设项目源码,评审分达到95分,都经过严格调试,确保可以运行!放心下载使用。 该项目资源主要针对计算机、自动化等相关专业的学生或从业者下载使用,也可作为期末课程设计、课程大作业、毕业设计等。 具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现类似其他功能。 一、说明 在某些涉及到数据安全的场景(如客户数据),为了防止脱库或者少数需要做数据安全防范的场景。需要针对数据做加密,但通常整库加密的时候直接使用数据库自带的加密即可。 本代码库基于MyBatis的数据库列加密库。现行很多解决方案要么是基于整库实施, 要么就是基于没有办法实现加密以后的LIKE查询。通常情况下,普通业务最多只需要对部分字段进行加密即可。因此,开发了这个库。 ### 设计思路 基于MyBatis的Alias与TypeHandler。其中,com.stableforever.mybatis.encryption.alias.EncryptedString为用于HOLD加密数据的类型Alias。com.stableforever.mybatis.encryption.handler.EncryptedStringHandler为具体的TypeHandler,用于处理数据库查询的入参时,自动把未加密的字符串转换为加密格式;同时,从数据库查询结果当中读取数据时自动把数据解密。 三、关于样例的说明 样例目录:examples/spring-boot-example/ 样例使用的数据库:mariadb(mysql同样适用) 数据库脚本:examples/spring-boot-example/src/main/resources/testdb.sql 如果你有更多的alias或者handler,根据mybatis的说明,你的spring boot配置当中可以使用逗号分隔,像这样写:xxxx.xxx,xxxx.xxxx 【备注】 更多项目详情见资源中的项目说明文件!!!
目 录 第一章 总 论 1 1.1 项目名称及承办单位 1 1.1.1 项目名称 1 1.1.2 承办单位 1 1.1.3 拟建场址 1 1.1.4 单位概况 1 1.2 编制依据 2 1.3 编制原则 3 1.4 项目建议书的工作范围 3 1.5 项目建议书的工作概况 4 1.6 项目建议书 4 1.6.1 建设方案 4 1.6.2 建设规模 12 1.6.3 主要经济指标 12 1.6.4 投资估算与资金筹措 13 1.6.5 综合评价结论 13 第二章 社会和市场调查与需求预测 14 2.1 社会和市场调查 14 2.1.1 产业政策 14 2.1.2 项目建设的必要性 14 2.2 项目发展概况 20 2.3 设计理念 20 2.4 市场预测 24 2.5 项目优势 25 2.6 市场定位 27 第三章 建设规模及建设方案 28 3.1 总平面规划 28 3.1.1 总平面规划原则 28 3.1.2 总平面规划综述 29 3.1.3 建设规模 31 3.2 建筑工程 31 3.2.1 建筑立面设计 31 3.2.2 小区内部环境设计 31 3.2.3 户型设计 32 3.2.4 内外装修 32 3.3 结构设计 32 3.3.1 设计依据 32 3.3.2 工程概况 33 3.3.3 结构设计 33 3.3.4 主要活荷取值 33 3.4 给排水设计 34 3.4.1 设计依据 34 3.4.2 工程概况 35 3.4.3 系统设计 35 3.4.4 管材 35 3.4.5 有待落实问题 35 3.5 暖通设计 36 3.5.1 设计依据 36 3.5.2 工程概况 36 3.5.3 系统设计 37 3.5.4采暖系统 37 3.5.5 采暖热媒及系统定压 37 3.5.6 通风系统 38 3.5.7 消防设计 38 3.5.8 环保与节能设计 38 3.6 电气工程设计 39 3.6.1 设计依据 39 3.6.2 主要设计内容 39 第四章 建设条件及气象条件 41 4.1 场地水、暖、电供应条件 41 4.4.1 供水 41 4.4.2 排水 41 4.4.3 供暖 41 4.4.4 供电 41 4.2 地质条件 41 4.3 场地气象条件 42 第五章 消防、警戒及社区网络 44 5.1 消防系统 44 5.2 消防报警 44 5.3 警戒监控 44 5.4 红外防盗报警系统 44 5.5 社区网络 44 第六章 环境保护 45 6.1 本项目对环境的影响 45 6.2 环境保护 45 6.3 三废治理方案 45 6.3.1 污水 45 6.3.2 生活垃圾 45 第七章 抗震、土地利用及绿化 46 7.1 抗震 46 7.2 土地利用 46 7.3 绿化 46 第八章 节能 47 8.1 建筑、结构 47 8.2 给排水供暖 47 8.3 电气 47 第九章 经营、管理与招投标 48 9.1 经营管理 48 9.2 服务管理 48 9.3 招投标 50 第十章 项目实施进度计划 51 10.1 项目实施内容 51 10.2 实施进度 51 第十一章 投资估算与资金筹措 52 11.1 投资估算范围 52 11.2 编制依据 52 11.3 编制方法及各部分费用确定 52 11.4 估算投资总额及其构成 54 11.5 资金筹措及投资计划 54 第十二章 经济效益分析 55 12.1 评价依据 55 12.2 基础数据 55 12.2.1 经营计划 55 12.2.2 实施进度 55 12.2.3 投资构成及分年度用款计划 55 12.2.4 收入估算 55 12.2.5 成本构成及估算 56 12.3 不确定性分析 57 12.4 临界点分析 57 12.5 综合评价 57 第十三章 结论 58 13.1 社会效益分析 58 13.2 经济效益评价 60 13.3 评价结论 62 13.4 建议 63

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值