精华内容
下载资源
问答
  • 睿智的目标检测30——Pytorch搭建YoloV4目标检测平台

    万次阅读 多人点赞 2020-05-24 14:14:13
    睿智的目标检测30——Pytorch搭建YoloV4目标检测平台学习前言什么是YOLOV4代码下载YOLOV4改进的部分(不完全)YOLOV4结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行预测4、预测...

    学习前言

    也做了一下pytorch版本的。
    在这里插入图片描述

    什么是YOLOV4

    在这里插入图片描述
    YOLOV4是YOLOV3的改进版,在YOLOV3的基础上结合了非常多的小Tricks。
    尽管没有目标检测上革命性的改变,但是YOLOV4依然很好的结合了速度与精度。
    根据上图也可以看出来,YOLOV4在YOLOV3的基础上,在FPS不下降的情况下,mAP达到了44,提高非常明显。

    YOLOV4整体上的检测思路和YOLOV3相比相差并不大,都是使用三个特征层进行分类与回归预测。

    请注意!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

    (重要的事情说三遍!)

    YOLOV3可参考该博客:
    https://blog.csdn.net/weixin_44791964/article/details/105310627

    代码下载

    https://github.com/bubbliiiing/yolov4-pytorch
    喜欢的可以给个star噢!

    YOLOV4改进的部分(不完全)

    1、主干特征提取网络:DarkNet53 => CSPDarkNet53

    2、特征金字塔:SPP,PAN

    3、分类回归层:YOLOv3(未改变)

    4、训练用到的小技巧:Mosaic数据增强、Label Smoothing平滑、CIOU、学习率余弦退火衰减

    5、激活函数:使用Mish激活函数

    以上并非全部的改进部分,还存在一些其它的改进,由于YOLOV4使用的改进实在太多了,很难完全实现与列出来,这里只列出来了一些我比较感兴趣,而且非常有效的改进。

    还有一个重要的事情:
    论文中提到的SAM,作者自己的源码也没有使用。

    还有其它很多的tricks,不是所有的tricks都有提升,我也没法实现全部的tricks

    整篇BLOG会结合YOLOV3与YOLOV4的差别进行解析

    YOLOV4结构解析

    为方便理解,本文将所有通道数都放到了最后一维度。
    为方便理解,本文将所有通道数都放到了最后一维度。
    为方便理解,本文将所有通道数都放到了最后一维度。

    1、主干特征提取网络Backbone

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    主干特征提取网络Backbone的改进点有两个:
    a).主干特征提取网络:DarkNet53 => CSPDarkNet53
    b).激活函数:使用Mish激活函数

    如果大家对YOLOV3比较熟悉的话,应该知道Darknet53的结构,其由一系列残差网络结构构成。在Darknet53中,其存在resblock_body模块,其由一次下采样多次残差结构的堆叠构成,Darknet53便是由resblock_body模块组合而成

    而在YOLOV4中,其对该部分进行了一定的修改。
    1、其一是将DarknetConv2D的激活函数由LeakyReLU修改成了Mish,卷积块由DarknetConv2D_BN_Leaky变成了DarknetConv2D_BN_Mish
    Mish函数的公式与图像如下:
    Mish=x×tanh(ln(1+ex)) Mish=x \times tanh(ln(1+e^x))
    在这里插入图片描述
    2、其二是将resblock_body的结构进行修改,使用了CSPnet结构。此时YOLOV4当中的Darknet53被修改成了CSPDarknet53
    在这里插入图片描述
    CSPnet结构并不算复杂,就是将原来的残差块的堆叠进行了一个拆分,拆成左右两部分:
    主干部分继续进行原来的残差块的堆叠
    另一部分则像一个残差边一样,经过少量处理直接连接到最后。
    因此可以认为CSP中存在一个大的残差边。

    #---------------------------------------------------#
    #   CSPdarknet的结构块
    #   存在一个大残差边
    #   这个大残差边绕过了很多的残差结构
    #---------------------------------------------------#
    class Resblock_body(nn.Module):
        def __init__(self, in_channels, out_channels, num_blocks, first):
            super(Resblock_body, self).__init__()
    
            self.downsample_conv = BasicConv(in_channels, out_channels, 3, stride=2)
    
            if first:
                self.split_conv0 = BasicConv(out_channels, out_channels, 1)
                self.split_conv1 = BasicConv(out_channels, out_channels, 1)  
                self.blocks_conv = nn.Sequential(
                    Resblock(channels=out_channels, hidden_channels=out_channels//2),
                    BasicConv(out_channels, out_channels, 1)
                )
                self.concat_conv = BasicConv(out_channels*2, out_channels, 1)
            else:
                self.split_conv0 = BasicConv(out_channels, out_channels//2, 1)
                self.split_conv1 = BasicConv(out_channels, out_channels//2, 1)
    
                self.blocks_conv = nn.Sequential(
                    *[Resblock(out_channels//2) for _ in range(num_blocks)],
                    BasicConv(out_channels//2, out_channels//2, 1)
                )
                self.concat_conv = BasicConv(out_channels, out_channels, 1)
    
        def forward(self, x):
            x = self.downsample_conv(x)
    
            x0 = self.split_conv0(x)
    
            x1 = self.split_conv1(x)
            x1 = self.blocks_conv(x1)
    
            x = torch.cat([x1, x0], dim=1)
            x = self.concat_conv(x)
    
            return x
    

    全部实现代码为:

    import torch
    import torch.nn.functional as F
    import torch.nn as nn
    import math
    from collections import OrderedDict
    
    #-------------------------------------------------#
    #   MISH激活函数
    #-------------------------------------------------#
    class Mish(nn.Module):
        def __init__(self):
            super(Mish, self).__init__()
    
        def forward(self, x):
            return x * torch.tanh(F.softplus(x))
    
    #-------------------------------------------------#
    #   卷积块
    #   CONV+BATCHNORM+MISH
    #-------------------------------------------------#
    class BasicConv(nn.Module):
        def __init__(self, in_channels, out_channels, kernel_size, stride=1):
            super(BasicConv, self).__init__()
    
            self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, kernel_size//2, bias=False)
            self.bn = nn.BatchNorm2d(out_channels)
            self.activation = Mish()
    
        def forward(self, x):
            x = self.conv(x)
            x = self.bn(x)
            x = self.activation(x)
            return x
    
    #---------------------------------------------------#
    #   CSPdarknet的结构块的组成部分
    #   内部堆叠的残差块
    #---------------------------------------------------#
    class Resblock(nn.Module):
        def __init__(self, channels, hidden_channels=None, residual_activation=nn.Identity()):
            super(Resblock, self).__init__()
    
            if hidden_channels is None:
                hidden_channels = channels
    
            self.block = nn.Sequential(
                BasicConv(channels, hidden_channels, 1),
                BasicConv(hidden_channels, channels, 3)
            )
    
        def forward(self, x):
            return x+self.block(x)
    
    #---------------------------------------------------#
    #   CSPdarknet的结构块
    #   存在一个大残差边
    #   这个大残差边绕过了很多的残差结构
    #---------------------------------------------------#
    class Resblock_body(nn.Module):
        def __init__(self, in_channels, out_channels, num_blocks, first):
            super(Resblock_body, self).__init__()
    
            self.downsample_conv = BasicConv(in_channels, out_channels, 3, stride=2)
    
            if first:
                self.split_conv0 = BasicConv(out_channels, out_channels, 1)
                self.split_conv1 = BasicConv(out_channels, out_channels, 1)  
                self.blocks_conv = nn.Sequential(
                    Resblock(channels=out_channels, hidden_channels=out_channels//2),
                    BasicConv(out_channels, out_channels, 1)
                )
                self.concat_conv = BasicConv(out_channels*2, out_channels, 1)
            else:
                self.split_conv0 = BasicConv(out_channels, out_channels//2, 1)
                self.split_conv1 = BasicConv(out_channels, out_channels//2, 1)
    
                self.blocks_conv = nn.Sequential(
                    *[Resblock(out_channels//2) for _ in range(num_blocks)],
                    BasicConv(out_channels//2, out_channels//2, 1)
                )
                self.concat_conv = BasicConv(out_channels, out_channels, 1)
    
        def forward(self, x):
            x = self.downsample_conv(x)
    
            x0 = self.split_conv0(x)
    
            x1 = self.split_conv1(x)
            x1 = self.blocks_conv(x1)
    
            x = torch.cat([x1, x0], dim=1)
            x = self.concat_conv(x)
    
            return x
    
    class CSPDarkNet(nn.Module):
        def __init__(self, layers):
            super(CSPDarkNet, self).__init__()
            self.inplanes = 32
            self.conv1 = BasicConv(3, self.inplanes, kernel_size=3, stride=1)
            self.feature_channels = [64, 128, 256, 512, 1024]
    
            self.stages = nn.ModuleList([
                Resblock_body(self.inplanes, self.feature_channels[0], layers[0], first=True),
                Resblock_body(self.feature_channels[0], self.feature_channels[1], layers[1], first=False),
                Resblock_body(self.feature_channels[1], self.feature_channels[2], layers[2], first=False),
                Resblock_body(self.feature_channels[2], self.feature_channels[3], layers[3], first=False),
                Resblock_body(self.feature_channels[3], self.feature_channels[4], layers[4], first=False)
            ])
    
            self.num_features = 1
            # 进行权值初始化
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                    m.weight.data.normal_(0, math.sqrt(2. / n))
                elif isinstance(m, nn.BatchNorm2d):
                    m.weight.data.fill_(1)
                    m.bias.data.zero_()
    
    
        def forward(self, x):
            x = self.conv1(x)
    
            x = self.stages[0](x)
            x = self.stages[1](x)
            out3 = self.stages[2](x)
            out4 = self.stages[3](out3)
            out5 = self.stages[4](out4)
    
            return out3, out4, out5
    
    def darknet53(pretrained, **kwargs):
        model = CSPDarkNet([1, 2, 8, 8, 4])
        if pretrained:
            if isinstance(pretrained, str):
                model.load_state_dict(torch.load(pretrained))
            else:
                raise Exception("darknet request a pretrained path. got [{}]".format(pretrained))
        return model
    

    2、特征金字塔

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    在特征金字塔部分,YOLOV4结合了两种改进:
    a).使用了SPP结构。
    b).使用了PANet结构。

    如上图所示,除去CSPDarknet53和Yolo Head的结构外,都是特征金字塔的结构。
    1、SPP结构参杂在对CSPdarknet53的最后一个特征层的卷积里,在对CSPdarknet53的最后一个特征层进行三次DarknetConv2D_BN_Leaky卷积后分别利用四个不同尺度的最大池化进行处理,最大池化的池化核大小分别为13x13、9x9、5x5、1x1(1x1即无处理)

    #---------------------------------------------------#
    #   SPP结构,利用不同大小的池化核进行池化
    #   池化后堆叠
    #---------------------------------------------------#
    class SpatialPyramidPooling(nn.Module):
        def __init__(self, pool_sizes=[5, 9, 13]):
            super(SpatialPyramidPooling, self).__init__()
    
            self.maxpools = nn.ModuleList([nn.MaxPool2d(pool_size, 1, pool_size//2) for pool_size in pool_sizes])
    
        def forward(self, x):
            features = [maxpool(x) for maxpool in self.maxpools[::-1]]
            features = torch.cat(features + [x], dim=1)
    
            return features
    

    其可以它能够极大地增加感受野,分离出最显著的上下文特征
    在这里插入图片描述
    2、PANet是2018的一种实例分割算法,其具体结构由反复提升特征的意思。
    在这里插入图片描述
    上图为原始的PANet的结构,可以看出来其具有一个非常重要的特点就是特征的反复提取
    在(a)里面是传统的特征金字塔结构,在完成特征金字塔从下到上的特征提取后,还需要实现(b)中从上到下的特征提取。

    而在YOLOV4当中,其主要是在三个有效特征层上使用了PANet结构。
    在这里插入图片描述
    实现代码如下:

    #---------------------------------------------------#
    #   yolo_body
    #---------------------------------------------------#
    class YoloBody(nn.Module):
        def __init__(self, config):
            super(YoloBody, self).__init__()
            self.config = config
            #  backbone
            self.backbone = darknet53(None)
    
            self.conv1 = make_three_conv([512,1024],1024)
            self.SPP = SpatialPyramidPooling()
            self.conv2 = make_three_conv([512,1024],2048)
    
            self.upsample1 = Upsample(512,256)
            self.conv_for_P4 = conv2d(512,256,1)
            self.make_five_conv1 = make_five_conv([256, 512],512)
    
            self.upsample2 = Upsample(256,128)
            self.conv_for_P3 = conv2d(256,128,1)
            self.make_five_conv2 = make_five_conv([128, 256],256)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter2 = len(config["yolo"]["anchors"][2]) * (5 + config["yolo"]["classes"])
            self.yolo_head3 = yolo_head([256, final_out_filter2],128)
    
            self.down_sample1 = conv2d(128,256,3,stride=2)
            self.make_five_conv3 = make_five_conv([256, 512],512)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter1 = len(config["yolo"]["anchors"][1]) * (5 + config["yolo"]["classes"])
            self.yolo_head2 = yolo_head([512, final_out_filter1],256)
    
    
            self.down_sample2 = conv2d(256,512,3,stride=2)
            self.make_five_conv4 = make_five_conv([512, 1024],1024)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter0 = len(config["yolo"]["anchors"][0]) * (5 + config["yolo"]["classes"])
            self.yolo_head1 = yolo_head([1024, final_out_filter0],512)
    
    
        def forward(self, x):
            #  backbone
            x2, x1, x0 = self.backbone(x)
    
            P5 = self.conv1(x0)
            P5 = self.SPP(P5)
            P5 = self.conv2(P5)
    
            P5_upsample = self.upsample1(P5)
            P4 = self.conv_for_P4(x1)
            P4 = torch.cat([P4,P5_upsample],axis=1)
            P4 = self.make_five_conv1(P4)
    
            P4_upsample = self.upsample2(P4)
            P3 = self.conv_for_P3(x2)
            P3 = torch.cat([P3,P4_upsample],axis=1)
            P3 = self.make_five_conv2(P3)
    
            P3_downsample = self.down_sample1(P3)
            P4 = torch.cat([P3_downsample,P4],axis=1)
            P4 = self.make_five_conv3(P4)
    
            P4_downsample = self.down_sample2(P4)
            P5 = torch.cat([P4_downsample,P5],axis=1)
            P5 = self.make_five_conv4(P5)
    
            out2 = self.yolo_head3(P3)
            out1 = self.yolo_head2(P4)
            out0 = self.yolo_head1(P5)
    
            return out0, out1, out2
    

    3、YoloHead利用获得到的特征进行预测

    当输入是416x416时,特征结构如下:
    在这里插入图片描述
    当输入是608x608时,特征结构如下:
    在这里插入图片描述
    1、在特征利用部分,YoloV4提取多特征层进行目标检测,一共提取三个特征层,分别位于中间层,中下层,底层,三个特征层的shape分别为(76,76,256)、(38,38,512)、(19,19,1024)。

    2、输出层的shape分别为(19,19,75),(38,38,75),(76,76,75),最后一个维度为75是因为该图是基于voc数据集的,它的类为20种,YoloV4只有针对每一个特征层存在3个先验框,所以最后维度为3x25;
    如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85
    ,三个特征层的shape为(19,19,255),(38,38,255),(76,76,255)

    实现代码如下:

    #---------------------------------------------------#
    #   最后获得yolov4的输出
    #---------------------------------------------------#
    def yolo_head(filters_list, in_filters):
        m = nn.Sequential(
            conv2d(in_filters, filters_list[0], 3),
            nn.Conv2d(filters_list[0], filters_list[1], 1),
        )
        return m
    
    #---------------------------------------------------#
    #   yolo_body
    #---------------------------------------------------#
    class YoloBody(nn.Module):
        def __init__(self, config):
            super(YoloBody, self).__init__()
            self.config = config
            #  backbone
            self.backbone = darknet53(None)
    
            self.conv1 = make_three_conv([512,1024],1024)
            self.SPP = SpatialPyramidPooling()
            self.conv2 = make_three_conv([512,1024],2048)
    
            self.upsample1 = Upsample(512,256)
            self.conv_for_P4 = conv2d(512,256,1)
            self.make_five_conv1 = make_five_conv([256, 512],512)
    
            self.upsample2 = Upsample(256,128)
            self.conv_for_P3 = conv2d(256,128,1)
            self.make_five_conv2 = make_five_conv([128, 256],256)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter2 = len(config["yolo"]["anchors"][2]) * (5 + config["yolo"]["classes"])
            self.yolo_head3 = yolo_head([256, final_out_filter2],128)
    
            self.down_sample1 = conv2d(128,256,3,stride=2)
            self.make_five_conv3 = make_five_conv([256, 512],512)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter1 = len(config["yolo"]["anchors"][1]) * (5 + config["yolo"]["classes"])
            self.yolo_head2 = yolo_head([512, final_out_filter1],256)
    
    
            self.down_sample2 = conv2d(256,512,3,stride=2)
            self.make_five_conv4 = make_five_conv([512, 1024],1024)
            # 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
            final_out_filter0 = len(config["yolo"]["anchors"][0]) * (5 + config["yolo"]["classes"])
            self.yolo_head1 = yolo_head([1024, final_out_filter0],512)
    
    
        def forward(self, x):
            #  backbone
            x2, x1, x0 = self.backbone(x)
    
            P5 = self.conv1(x0)
            P5 = self.SPP(P5)
            P5 = self.conv2(P5)
    
            P5_upsample = self.upsample1(P5)
            P4 = self.conv_for_P4(x1)
            P4 = torch.cat([P4,P5_upsample],axis=1)
            P4 = self.make_five_conv1(P4)
    
            P4_upsample = self.upsample2(P4)
            P3 = self.conv_for_P3(x2)
            P3 = torch.cat([P3,P4_upsample],axis=1)
            P3 = self.make_five_conv2(P3)
    
            P3_downsample = self.down_sample1(P3)
            P4 = torch.cat([P3_downsample,P4],axis=1)
            P4 = self.make_five_conv3(P4)
    
            P4_downsample = self.down_sample2(P4)
            P5 = torch.cat([P4_downsample,P5],axis=1)
            P5 = self.make_five_conv4(P5)
    
            out2 = self.yolo_head3(P3)
            out1 = self.yolo_head2(P4)
            out0 = self.yolo_head1(P5)
    
            return out0, out1, out2
    

    4、预测结果的解码

    由第二步我们可以获得三个特征层的预测结果,shape分别为(N,19,19,255),(N,38,38,255),(N,76,76,255)的数据,对应每个图分为19x19、38x38、76x76的网格上3个预测框的位置。

    但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。

    此处要讲一下yolo3的预测原理,yolo3的3个特征层分别将整幅图分为19x19、38x38、76x76的网格,每个网络点负责一个区域的检测。

    我们知道特征层的预测结果对应着三个预测框的位置,我们先将其reshape一下,其结果为(N,19,19,3,85),(N,38,38,3,85),(N,76,76,3,85)。

    最后一个维度中的85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果。

    yolo3的解码过程就是将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

    在这里插入图片描述
    当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选
    这一部分基本上是所有目标检测通用的部分。不过该项目的处理方式与其它项目不同。其对于每一个类进行判别。
    1、取出每一类得分大于self.obj_threshold的框和得分。
    2、利用框的位置和得分进行非极大抑制。

    实现代码如下,当调用yolo_eval时,就会对每个特征层进行解码:

    class DecodeBox(nn.Module):
        def __init__(self, anchors, num_classes, img_size):
            super(DecodeBox, self).__init__()
            self.anchors = anchors
            self.num_anchors = len(anchors)
            self.num_classes = num_classes
            self.bbox_attrs = 5 + num_classes
            self.img_size = img_size
    
        def forward(self, input):
            # input为bs,3*(1+4+num_classes),13,13
    
            # 一共多少张图片
            batch_size = input.size(0)
            # 13,13
            input_height = input.size(2)
            input_width = input.size(3)
    
            # 计算步长
            # 每一个特征点对应原来的图片上多少个像素点
            # 如果特征层为13x13的话,一个特征点就对应原来的图片上的32个像素点
            # 416/13 = 32
            stride_h = self.img_size[1] / input_height
            stride_w = self.img_size[0] / input_width
    
            # 把先验框的尺寸调整成特征层大小的形式
            # 计算出先验框在特征层上对应的宽高
            scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors]
    
            # bs,3*(5+num_classes),13,13 -> bs,3,13,13,(5+num_classes)
            prediction = input.view(batch_size, self.num_anchors,
                                    self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous()
    
            # 先验框的中心位置的调整参数
            x = torch.sigmoid(prediction[..., 0])  
            y = torch.sigmoid(prediction[..., 1])
            # 先验框的宽高调整参数
            w = prediction[..., 2]  # Width
            h = prediction[..., 3]  # Height
    
            # 获得置信度,是否有物体
            conf = torch.sigmoid(prediction[..., 4])
            # 种类置信度
            pred_cls = torch.sigmoid(prediction[..., 5:])  # Cls pred.
    
            FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor
            LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
    
            # 生成网格,先验框中心,网格左上角 batch_size,3,13,13
            grid_x = torch.linspace(0, input_width - 1, input_width).repeat(input_width, 1).repeat(
                batch_size * self.num_anchors, 1, 1).view(x.shape).type(FloatTensor)
            grid_y = torch.linspace(0, input_height - 1, input_height).repeat(input_height, 1).t().repeat(
                batch_size * self.num_anchors, 1, 1).view(y.shape).type(FloatTensor)
    
            # 生成先验框的宽高
            anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0]))
            anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1]))
            anchor_w = anchor_w.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(w.shape)
            anchor_h = anchor_h.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(h.shape)
            
            # 计算调整后的先验框中心与宽高
            pred_boxes = FloatTensor(prediction[..., :4].shape)
            pred_boxes[..., 0] = x.data + grid_x
            pred_boxes[..., 1] = y.data + grid_y
            pred_boxes[..., 2] = torch.exp(w.data) * anchor_w
            pred_boxes[..., 3] = torch.exp(h.data) * anchor_h
    
            # 用于将输出调整为相对于416x416的大小
            _scale = torch.Tensor([stride_w, stride_h] * 2).type(FloatTensor)
            output = torch.cat((pred_boxes.view(batch_size, -1, 4) * _scale,
                                conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_classes)), -1)
            return output.data
    

    5、在原图上进行绘制

    通过第四步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

    YOLOV4的训练

    1、YOLOV4的改进训练技巧

    a)、Mosaic数据增强

    Yolov4的mosaic数据增强参考了CutMix数据增强方式,理论上具有一定的相似性!
    CutMix数据增强方式利用两张图片进行拼接。
    在这里插入图片描述
    但是mosaic利用了四张图片,根据论文所说其拥有一个巨大的优点是丰富检测物体的背景!且在BN计算的时候一下子会计算四张图片的数据!
    就像下图这样:
    在这里插入图片描述
    实现思路如下:
    1、每次读取四张图片。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    2、分别对四张图片进行翻转、缩放、色域变化等,并且按照四个方向位置摆好。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    3、进行图片的组合和框的组合
    在这里插入图片描述

    def rand(a=0, b=1):
        return np.random.rand()*(b-a) + a
    
    def merge_bboxes(bboxes, cutx, cuty):
    
        merge_bbox = []
        for i in range(len(bboxes)):
            for box in bboxes[i]:
                tmp_box = []
                x1,y1,x2,y2 = box[0], box[1], box[2], box[3]
    
                if i == 0:
                    if y1 > cuty or x1 > cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
                        if y2-y1 < 5:
                            continue
                    if x2 >= cutx and x1 <= cutx:
                        x2 = cutx
                        if x2-x1 < 5:
                            continue
                    
                if i == 1:
                    if y2 < cuty or x1 > cutx:
                        continue
    
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                        if y2-y1 < 5:
                            continue
                    
                    if x2 >= cutx and x1 <= cutx:
                        x2 = cutx
                        if x2-x1 < 5:
                            continue
    
                if i == 2:
                    if y2 < cuty or x2 < cutx:
                        continue
    
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                        if y2-y1 < 5:
                            continue
    
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx
                        if x2-x1 < 5:
                            continue
    
                if i == 3:
                    if y1 > cuty or x2 < cutx:
                        continue
    
                    if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
                        if y2-y1 < 5:
                            continue
    
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx
                        if x2-x1 < 5:
                            continue
    
                tmp_box.append(x1)
                tmp_box.append(y1)
                tmp_box.append(x2)
                tmp_box.append(y2)
                tmp_box.append(box[-1])
                merge_bbox.append(tmp_box)
        return merge_bbox
    
    def get_random_data(annotation_line, input_shape, random=True, hue=.1, sat=1.5, val=1.5, proc_img=True):
        '''random preprocessing for real-time data augmentation'''
        h, w = input_shape
        min_offset_x = 0.4
        min_offset_y = 0.4
        scale_low = 1-min(min_offset_x,min_offset_y)
        scale_high = scale_low+0.2
    
        image_datas = [] 
        box_datas = []
        index = 0
    
        place_x = [0,0,int(w*min_offset_x),int(w*min_offset_x)]
        place_y = [0,int(h*min_offset_y),int(w*min_offset_y),0]
        for line in annotation_line:
            # 每一行进行分割
            line_content = line.split()
            # 打开图片
            image = Image.open(line_content[0])
            image = image.convert("RGB") 
            # 图片的大小
            iw, ih = image.size
            # 保存框的位置
            box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]])
            
            # image.save(str(index)+".jpg")
            # 是否翻转图片
            flip = rand()<.5
            if flip and len(box)>0:
                image = image.transpose(Image.FLIP_LEFT_RIGHT)
                box[:, [0,2]] = iw - box[:, [2,0]]
    
            # 对输入进来的图片进行缩放
            new_ar = w/h
            scale = rand(scale_low, scale_high)
            if new_ar < 1:
                nh = int(scale*h)
                nw = int(nh*new_ar)
            else:
                nw = int(scale*w)
                nh = int(nw/new_ar)
            image = image.resize((nw,nh), Image.BICUBIC)
    
            # 进行色域变换
            hue = rand(-hue, hue)
            sat = rand(1, sat) if rand()<.5 else 1/rand(1, sat)
            val = rand(1, val) if rand()<.5 else 1/rand(1, val)
            x = rgb_to_hsv(np.array(image)/255.)
            x[..., 0] += hue
            x[..., 0][x[..., 0]>1] -= 1
            x[..., 0][x[..., 0]<0] += 1
            x[..., 1] *= sat
            x[..., 2] *= val
            x[x>1] = 1
            x[x<0] = 0
            image = hsv_to_rgb(x)
    
            image = Image.fromarray((image*255).astype(np.uint8))
            # 将图片进行放置,分别对应四张分割图片的位置
            dx = place_x[index]
            dy = place_y[index]
            new_image = Image.new('RGB', (w,h), (128,128,128))
            new_image.paste(image, (dx, dy))
            image_data = np.array(new_image)/255
    
            # Image.fromarray((image_data*255).astype(np.uint8)).save(str(index)+"distort.jpg")
            
            index = index + 1
            box_data = []
            # 对box进行重新处理
            if len(box)>0:
                np.random.shuffle(box)
                box[:, [0,2]] = box[:, [0,2]]*nw/iw + dx
                box[:, [1,3]] = box[:, [1,3]]*nh/ih + dy
                box[:, 0:2][box[:, 0:2]<0] = 0
                box[:, 2][box[:, 2]>w] = w
                box[:, 3][box[:, 3]>h] = h
                box_w = box[:, 2] - box[:, 0]
                box_h = box[:, 3] - box[:, 1]
                box = box[np.logical_and(box_w>1, box_h>1)]
                box_data = np.zeros((len(box),5))
                box_data[:len(box)] = box
            
            image_datas.append(image_data)
            box_datas.append(box_data)
    
            img = Image.fromarray((image_data*255).astype(np.uint8))
            for j in range(len(box_data)):
                thickness = 3
                left, top, right, bottom  = box_data[j][0:4]
                draw = ImageDraw.Draw(img)
                for i in range(thickness):
                    draw.rectangle([left + i, top + i, right - i, bottom - i],outline=(255,255,255))
            img.show()
    
        
        # 将图片分割,放在一起
        cutx = np.random.randint(int(w*min_offset_x), int(w*(1 - min_offset_x)))
        cuty = np.random.randint(int(h*min_offset_y), int(h*(1 - min_offset_y)))
    
        new_image = np.zeros([h,w,3])
        new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
        new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
        new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
        new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :]
    
        # 对框进行进一步的处理
        new_boxes = merge_bboxes(box_datas, cutx, cuty)
    
        return new_image, new_boxes
    

    b)、Label Smoothing平滑

    标签平滑的思想很简单,具体公式如下:

    new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes
    

    当label_smoothing的值为0.01得时候,公式变成如下所示:

    new_onehot_labels = y * (1 - 0.01) + 0.01 / num_classes
    

    其实Label Smoothing平滑就是将标签进行一个平滑,原始的标签是0、1,在平滑后变成0.005(如果是二分类)、0.995,也就是说对分类准确做了一点惩罚,让模型不可以分类的太准确,太准确容易过拟合。

    实现代码如下:

    #---------------------------------------------------#
    #   平滑标签
    #---------------------------------------------------#
    def smooth_labels(y_true, label_smoothing,num_classes):
        return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
    

    c)、CIOU

    IoU是比值的概念,对目标物体的scale是不敏感的。然而常用的BBox的回归损失优化和IoU优化不是完全等价的,寻常的IoU无法直接优化没有重叠的部分。

    于是有人提出直接使用IOU作为回归优化loss,CIOU是其中非常优秀的一种想法。

    CIOU将目标与anchor之间的距离,重叠率、尺度以及惩罚项都考虑进去,使得目标框回归变得更加稳定,不会像IoU和GIoU一样出现训练过程中发散等问题。而惩罚因子把预测框长宽比拟合目标框的长宽比考虑进去。

    在这里插入图片描述
    CIOU公式如下
    CIOU=IOUρ2(b,bgt)c2αv CIOU = IOU - \frac{\rho^2(b,b^{gt})}{c^2} - \alpha v
    其中,ρ2(b,bgt)\rho^2(b,b^{gt})分别代表了预测框和真实框的中心点的欧式距离。 c代表的是能够同时包含预测框和真实框的最小闭包区域的对角线距离。

    α\alphavv的公式如下
    α=v1IOU+v \alpha = \frac{v}{1-IOU+v}
    v=4π2(arctanwgthgtarctanwh)2 v = \frac{4}{\pi ^2}(arctan\frac{w^{gt}}{h^{gt}}-arctan\frac{w}{h})^2
    把1-CIOU就可以得到相应的LOSS了。
    LOSSCIOU=1IOU+ρ2(b,bgt)c2+αv LOSS_{CIOU} = 1 - IOU + \frac{\rho^2(b,b^{gt})}{c^2} + \alpha v

    def box_ciou(b1, b2):
        """
        输入为:
        ----------
        b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
        b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    
        返回为:
        -------
        ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
        """
        # 求出预测框左上角右下角
        b1_xy = b1[..., :2]
        b1_wh = b1[..., 2:4]
        b1_wh_half = b1_wh/2.
        b1_mins = b1_xy - b1_wh_half
        b1_maxes = b1_xy + b1_wh_half
        # 求出真实框左上角右下角
        b2_xy = b2[..., :2]
        b2_wh = b2[..., 2:4]
        b2_wh_half = b2_wh/2.
        b2_mins = b2_xy - b2_wh_half
        b2_maxes = b2_xy + b2_wh_half
    
        # 求真实框和预测框所有的iou
        intersect_mins = torch.max(b1_mins, b2_mins)
        intersect_maxes = torch.min(b1_maxes, b2_maxes)
        intersect_wh = torch.max(intersect_maxes - intersect_mins, torch.zeros_like(intersect_maxes))
        intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
        b1_area = b1_wh[..., 0] * b1_wh[..., 1]
        b2_area = b2_wh[..., 0] * b2_wh[..., 1]
        union_area = b1_area + b2_area - intersect_area
        iou = intersect_area / (union_area + 1e-6)
    
        # 计算中心的差距
        center_distance = torch.sum(torch.pow((b1_xy - b2_xy), 2), axis=-1)
        # 找到包裹两个框的最小框的左上角和右下角
        enclose_mins = torch.min(b1_mins, b2_mins)
        enclose_maxes = torch.max(b1_maxes, b2_maxes)
        enclose_wh = torch.max(enclose_maxes - enclose_mins, torch.zeros_like(intersect_maxes))
        # 计算对角线距离
        enclose_diagonal = torch.sum(torch.pow(enclose_wh,2), axis=-1)
        ciou = iou - 1.0 * (center_distance) / (enclose_diagonal + 1e-7)
        
        v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(b1_wh[..., 0]/b1_wh[..., 1]) - torch.atan(b2_wh[..., 0]/b2_wh[..., 1])), 2)
        alpha = v / (1.0 - iou + v)
        ciou = ciou - alpha * v
        return ciou
    

    d)、学习率余弦退火衰减

    余弦退火衰减法,学习率会先上升再下降,这是退火优化法的思想。(关于什么是退火算法可以百度。)

    上升的时候使用线性上升,下降的时候模拟cos函数下降。执行多次。

    效果如图所示:
    在这里插入图片描述
    pytorch有直接实现的函数,可直接调用。

    lr_scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=5, eta_min=1e-5)
    

    2、loss组成

    a)、计算loss所需参数

    在计算loss的时候,实际上是y_pre和y_true之间的对比:
    y_pre就是一幅图像经过网络之后的输出,内部含有三个特征层的内容;其需要解码才能够在图上作画
    y_true就是一个真实图像中,它的每个真实框对应的(19,19)、(38,38)、(76,76)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
    实际上y_pre和y_true内容的shape都是
    (batch_size,19,19,3,85)
    (batch_size,38,38,3,85)
    (batch_size,76,76,3,85)

    b)、y_pre是什么

    网络最后输出的内容就是三个特征层每个网格点对应的预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后,每个网格点上三个先验框对应的位置、置信度及其种类。
    对于输出的y1、y2、y3而言,[…, : 2]指的是相对于每个网格点的偏移量,[…, 2: 4]指的是宽和高,[…, 4: 5]指的是该框的置信度,[…, 5: ]指的是每个种类的预测概率。
    现在的y_pre还是没有解码的,解码了之后才是真实图像上的情况。

    c)、y_true是什么。

    y_true就是一个真实图像中,它的每个真实框对应的(19,19)、(38,38)、(76,76)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致

    d)、loss的计算过程

    在得到了y_pre和y_true后怎么对比呢?不是简单的减一下!

    loss值需要对三个特征层进行处理,这里以最小的特征层为例。
    1、利用y_true取出该特征层中真实存在目标的点的位置(m,19,19,3,1)及其对应的种类(m,19,19,3,80)。
    2、将prediction的预测值输出进行处理,得到reshape后的预测值y_pre,shape为(m,19,19,3,85)。还有解码后的xy,wh。
    3、对于每一幅图,计算其中所有真实框与预测框的IOU,如果某些预测框和真实框的重合程度大于0.5,则忽略。
    4、计算ciou作为回归的loss,这里只计算正样本的回归loss。
    5、计算置信度的loss,其有两部分构成,第一部分是实际上存在目标的,预测结果中置信度的值与1对比;第二部分是实际上不存在目标的,在第四步中得到其最大IOU的值与0对比。
    6、计算预测种类的loss,其计算的是实际上存在目标的,预测类与真实类的差距。

    其实际上计算的总的loss是三个loss的和,这三个loss分别是:

    • 实际存在的框,CIOU LOSS
    • 实际存在的框,预测结果中置信度的值与1对比;实际不存在的框,预测结果中置信度的值与0对比,该部分要去除被忽略的不包含目标的框
    • 实际存在的框,种类预测结果与实际结果的对比

    其实际代码如下:

    
    #---------------------------------------------------#
    #   平滑标签
    #---------------------------------------------------#
    def smooth_labels(y_true, label_smoothing,num_classes):
        return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes
    
    def box_ciou(b1, b2):
        """
        输入为:
        ----------
        b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
        b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    
        返回为:
        -------
        ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
        """
        # 求出预测框左上角右下角
        b1_xy = b1[..., :2]
        b1_wh = b1[..., 2:4]
        b1_wh_half = b1_wh/2.
        b1_mins = b1_xy - b1_wh_half
        b1_maxes = b1_xy + b1_wh_half
        # 求出真实框左上角右下角
        b2_xy = b2[..., :2]
        b2_wh = b2[..., 2:4]
        b2_wh_half = b2_wh/2.
        b2_mins = b2_xy - b2_wh_half
        b2_maxes = b2_xy + b2_wh_half
    
        # 求真实框和预测框所有的iou
        intersect_mins = torch.max(b1_mins, b2_mins)
        intersect_maxes = torch.min(b1_maxes, b2_maxes)
        intersect_wh = torch.max(intersect_maxes - intersect_mins, torch.zeros_like(intersect_maxes))
        intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
        b1_area = b1_wh[..., 0] * b1_wh[..., 1]
        b2_area = b2_wh[..., 0] * b2_wh[..., 1]
        union_area = b1_area + b2_area - intersect_area
        iou = intersect_area / (union_area + 1e-6)
    
        # 计算中心的差距
        center_distance = torch.sum(torch.pow((b1_xy - b2_xy), 2), axis=-1)
        # 找到包裹两个框的最小框的左上角和右下角
        enclose_mins = torch.min(b1_mins, b2_mins)
        enclose_maxes = torch.max(b1_maxes, b2_maxes)
        enclose_wh = torch.max(enclose_maxes - enclose_mins, torch.zeros_like(intersect_maxes))
        # 计算对角线距离
        enclose_diagonal = torch.sum(torch.pow(enclose_wh,2), axis=-1)
        ciou = iou - 1.0 * (center_distance) / (enclose_diagonal + 1e-7)
        
        v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(b1_wh[..., 0]/b1_wh[..., 1]) - torch.atan(b2_wh[..., 0]/b2_wh[..., 1])), 2)
        alpha = v / (1.0 - iou + v)
        ciou = ciou - alpha * v
        return ciou
    
    def clip_by_tensor(t,t_min,t_max):
        t=t.float()
        result = (t >= t_min).float() * t + (t < t_min).float() * t_min
        result = (result <= t_max).float() * result + (result > t_max).float() * t_max
        return result
    
    def MSELoss(pred,target):
        return (pred-target)**2
    
    def BCELoss(pred,target):
        epsilon = 1e-7
        pred = clip_by_tensor(pred, epsilon, 1.0 - epsilon)
        output = -target * torch.log(pred) - (1.0 - target) * torch.log(1.0 - pred)
        return output
    
    class YOLOLoss(nn.Module):
        def __init__(self, anchors, num_classes, img_size, label_smooth=0, cuda=True):
            super(YOLOLoss, self).__init__()
            self.anchors = anchors
            self.num_anchors = len(anchors)
            self.num_classes = num_classes
            self.bbox_attrs = 5 + num_classes
            self.img_size = img_size
            self.label_smooth = label_smooth
    
            self.ignore_threshold = 0.5
            self.lambda_conf = 1.0
            self.lambda_cls = 1.0
            self.lambda_loc = 1.0
            self.cuda = cuda
    
        def forward(self, input, targets=None):
            # input为bs,3*(5+num_classes),13,13
            
            # 一共多少张图片
            bs = input.size(0)
            # 特征层的高
            in_h = input.size(2)
            # 特征层的宽
            in_w = input.size(3)
    
            # 计算步长
            # 每一个特征点对应原来的图片上多少个像素点
            # 如果特征层为13x13的话,一个特征点就对应原来的图片上的32个像素点
            stride_h = self.img_size[1] / in_h
            stride_w = self.img_size[0] / in_w
    
            # 把先验框的尺寸调整成特征层大小的形式
            # 计算出先验框在特征层上对应的宽高
            scaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in self.anchors]
            # bs,3*(5+num_classes),13,13 -> bs,3,13,13,(5+num_classes)
            prediction = input.view(bs, int(self.num_anchors/3),
                                    self.bbox_attrs, in_h, in_w).permute(0, 1, 3, 4, 2).contiguous()
            
            # 对prediction预测进行调整
            conf = torch.sigmoid(prediction[..., 4])  # Conf
            pred_cls = torch.sigmoid(prediction[..., 5:])  # Cls pred.
    
            # 找到哪些先验框内部包含物体
            mask, noobj_mask, t_box, tconf, tcls, box_loss_scale_x, box_loss_scale_y = self.get_target(targets, scaled_anchors,in_w, in_h,self.ignore_threshold)
    
            noobj_mask, pred_boxes_for_ciou = self.get_ignore(prediction, targets, scaled_anchors, in_w, in_h, noobj_mask)
    
            if self.cuda:
                mask, noobj_mask = mask.cuda(), noobj_mask.cuda()
                box_loss_scale_x, box_loss_scale_y= box_loss_scale_x.cuda(), box_loss_scale_y.cuda()
                tconf, tcls = tconf.cuda(), tcls.cuda()
                pred_boxes_for_ciou = pred_boxes_for_ciou.cuda()
                t_box = t_box.cuda()
    
            box_loss_scale = 2-box_loss_scale_x*box_loss_scale_y
            #  losses.
            ciou = (1 - box_ciou( pred_boxes_for_ciou[mask.bool()], t_box[mask.bool()]))* box_loss_scale[mask.bool()]
    
            loss_loc = torch.sum(ciou / bs)
            loss_conf = torch.sum(BCELoss(conf, mask) * mask / bs) + \
                        torch.sum(BCELoss(conf, mask) * noobj_mask / bs)
                        
            # print(smooth_labels(tcls[mask == 1],self.label_smooth,self.num_classes))
            loss_cls = torch.sum(BCELoss(pred_cls[mask == 1], smooth_labels(tcls[mask == 1],self.label_smooth,self.num_classes))/bs)
            # print(loss_loc,loss_conf,loss_cls)
            loss = loss_conf * self.lambda_conf + loss_cls * self.lambda_cls + loss_loc * self.lambda_loc
            return loss, loss_conf.item(), loss_cls.item(), loss_loc.item()
    
        def get_target(self, target, anchors, in_w, in_h, ignore_threshold):
            # 计算一共有多少张图片
            bs = len(target)
            # 获得先验框
            anchor_index = [[0,1,2],[3,4,5],[6,7,8]][[13,26,52].index(in_w)]
            subtract_index = [0,3,6][[13,26,52].index(in_w)]
            # 创建全是0或者全是1的阵列
            mask = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            noobj_mask = torch.ones(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
    
            tx = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            ty = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            tw = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            th = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            t_box = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, 4, requires_grad=False)
            tconf = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            tcls = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, self.num_classes, requires_grad=False)
    
            box_loss_scale_x = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            box_loss_scale_y = torch.zeros(bs, int(self.num_anchors/3), in_h, in_w, requires_grad=False)
            for b in range(bs):
                for t in range(target[b].shape[0]):
                    # 计算出在特征层上的点位
                    gx = target[b][t, 0] * in_w
                    gy = target[b][t, 1] * in_h
                    
                    gw = target[b][t, 2] * in_w
                    gh = target[b][t, 3] * in_h
    
                    # 计算出属于哪个网格
                    gi = int(gx)
                    gj = int(gy)
    
                    # 计算真实框的位置
                    gt_box = torch.FloatTensor(np.array([0, 0, gw, gh])).unsqueeze(0)
                    
                    # 计算出所有先验框的位置
                    anchor_shapes = torch.FloatTensor(np.concatenate((np.zeros((self.num_anchors, 2)),
                                                                      np.array(anchors)), 1))
                    # 计算重合程度
                    anch_ious = bbox_iou(gt_box, anchor_shapes)
                   
                    # Find the best matching anchor box
                    best_n = np.argmax(anch_ious)
                    if best_n not in anchor_index:
                        continue
                    # Masks
                    if (gj < in_h) and (gi < in_w):
                        best_n = best_n - subtract_index
                        # 判定哪些先验框内部真实的存在物体
                        noobj_mask[b, best_n, gj, gi] = 0
                        mask[b, best_n, gj, gi] = 1
                        # 计算先验框中心调整参数
                        tx[b, best_n, gj, gi] = gx
                        ty[b, best_n, gj, gi] = gy
                        # 计算先验框宽高调整参数
                        tw[b, best_n, gj, gi] = gw
                        th[b, best_n, gj, gi] = gh
                        # 用于获得xywh的比例
                        box_loss_scale_x[b, best_n, gj, gi] = target[b][t, 2]
                        box_loss_scale_y[b, best_n, gj, gi] = target[b][t, 3]
                        # 物体置信度
                        tconf[b, best_n, gj, gi] = 1
                        # 种类
                        tcls[b, best_n, gj, gi, int(target[b][t, 4])] = 1
                    else:
                        print('Step {0} out of bound'.format(b))
                        print('gj: {0}, height: {1} | gi: {2}, width: {3}'.format(gj, in_h, gi, in_w))
                        continue
            t_box[...,0] = tx
            t_box[...,1] = ty
            t_box[...,2] = tw
            t_box[...,3] = th
            return mask, noobj_mask, t_box, tconf, tcls, box_loss_scale_x, box_loss_scale_y
    
        def get_ignore(self,prediction,target,scaled_anchors,in_w, in_h,noobj_mask):
            bs = len(target)
            anchor_index = [[0,1,2],[3,4,5],[6,7,8]][[13,26,52].index(in_w)]
            scaled_anchors = np.array(scaled_anchors)[anchor_index]
            # 先验框的中心位置的调整参数
            x = torch.sigmoid(prediction[..., 0])  
            y = torch.sigmoid(prediction[..., 1])
            # 先验框的宽高调整参数
            w = prediction[..., 2]  # Width
            h = prediction[..., 3]  # Height
    
            FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor
            LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
    
            # 生成网格,先验框中心,网格左上角
            grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_w, 1).repeat(
                int(bs*self.num_anchors/3), 1, 1).view(x.shape).type(FloatTensor)
            grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_h, 1).t().repeat(
                int(bs*self.num_anchors/3), 1, 1).view(y.shape).type(FloatTensor)
    
            # 生成先验框的宽高
            anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0]))
            anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1]))
            
            anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape)
            anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape)
            
            # 计算调整后的先验框中心与宽高
            pred_boxes = FloatTensor(prediction[..., :4].shape)
            pred_boxes[..., 0] = x + grid_x
            pred_boxes[..., 1] = y + grid_y
            pred_boxes[..., 2] = torch.exp(w) * anchor_w
            pred_boxes[..., 3] = torch.exp(h) * anchor_h
            for i in range(bs):
                pred_boxes_for_ignore = pred_boxes[i]
                pred_boxes_for_ignore = pred_boxes_for_ignore.view(-1, 4)
    
                for t in range(target[i].shape[0]):
                    gx = target[i][t, 0] * in_w
                    gy = target[i][t, 1] * in_h
                    gw = target[i][t, 2] * in_w
                    gh = target[i][t, 3] * in_h
                    gt_box = torch.FloatTensor(np.array([gx, gy, gw, gh])).unsqueeze(0).type(FloatTensor)
    
                    anch_ious = bbox_iou(gt_box, pred_boxes_for_ignore, x1y1x2y2=False)
                    anch_ious = anch_ious.view(pred_boxes[i].size()[:3])
                    noobj_mask[i][anch_ious>self.ignore_threshold] = 0
            return noobj_mask, pred_boxes
    

    训练自己的YOLOV4模型

    yolo4整体的文件夹构架如下:
    在这里插入图片描述
    本文使用VOC格式进行训练。
    训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。
    在这里插入图片描述
    训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
    在这里插入图片描述
    在训练前利用voc2yolo3.py文件生成对应的txt。
    在这里插入图片描述
    再运行根目录下的voc_annotation.py,运行前需要将classes改成你自己的classes。

    classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
    

    在这里插入图片描述
    就会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。
    在这里插入图片描述
    在训练前需要修改model_data里面的voc_classes.txt文件,需要将classes改成你自己的classes。
    在这里插入图片描述
    运行train.py即可开始训练。
    在这里插入图片描述

    展开全文
  • 目标检测】Faster RCNN算法详解

    万次阅读 多人点赞 2016-04-21 15:08:06
    继RCNN,fast RCNN之后,目标检测界的领军人物Ross Girshick在2015年提出faster RCNN。目标检测速度达到15fps。

    Ren, Shaoqing, et al. “Faster R-CNN: Towards real-time object detection with region proposal networks.” Advances in Neural Information Processing Systems. 2015.

    本文是继RCNN[1],fast RCNN[2]之后,目标检测界的领军人物Ross Girshick团队在2015年的又一力作。简单网络目标检测速度达到17fps,在PASCAL VOC上准确率为59.9%;复杂网络达到5fps,准确率78.8%。

    作者在github上给出了基于matlabpython的源码。对Region CNN算法不了解的同学,请先参看这两篇文章:《RCNN算法详解》《fast RCNN算法详解》

    思想

    从RCNN到fast RCNN,再到本文的faster RCNN,目标检测的四个基本步骤(候选区域生成,特征提取,分类,位置精修)终于被统一到一个深度网络框架之内。所有计算没有重复,完全在GPU中完成,大大提高了运行速度。
    这里写图片描述

    faster RCNN可以简单地看做“区域生成网络+fast RCNN“的系统,用区域生成网络代替fast RCNN中的Selective Search方法。本篇论文着重解决了这个系统中的三个问题:

    1. 如何设计区域生成网络
    2. 如何训练区域生成网络
    3. 如何让区域生成网络和fast RCNN网络共享特征提取网络

    区域生成网络:结构

    基本设想是:在提取好的特征图上,对所有可能的候选框进行判别。由于后续还有位置精修步骤,所以候选框实际比较稀疏。
    这里写图片描述

    特征提取

    原始特征提取(上图灰色方框)包含若干层conv+relu,直接套用ImageNet上常见的分类网络即可。本文试验了两种网络:5层的ZF[3],16层的VGG-16[[^-4]],具体结构不再赘述。
    额外添加一个conv+relu层,输出5139256维特征(feature)。

    候选区域(anchor)

    特征可以看做一个尺度5139的256通道图像,对于该图像的每一个位置,考虑9个可能的候选窗口:三种面积{1282,2562,5122}×\{128^2, 256^2, 512^2 \}\times三种比例{1:1,1:2,2:1}\{ 1:1, 1:2, 2:1\}。这些候选窗口称为anchors。下图示出5139个anchor中心,以及9种anchor示例。
    这里写图片描述

    在整个faster RCNN算法中,有三种尺度。
    原图尺度:原始输入的大小。不受任何限制,不影响性能。
    归一化尺度:输入特征提取网络的大小,在测试时设置,源码中opts.test_scale=600。anchor在这个尺度上设定。这个参数和anchor的相对大小决定了想要检测的目标范围。
    网络输入尺度:输入特征检测网络的大小,在训练时设置,源码中为224*224。

    窗口分类和位置精修

    分类层(cls_score)输出每一个位置上,9个anchor属于前景和背景的概率;窗口回归层(bbox_pred)输出每一个位置上,9个anchor对应窗口应该平移缩放的参数。
    对于每一个位置来说,分类层从256维特征中输出属于前景和背景的概率;窗口回归层从256维特征中输出4个平移缩放参数。

    就局部来说,这两层是全连接网络;就全局来说,由于网络在所有位置(共51*39个)的参数相同,所以实际用尺寸为1×1的卷积网络实现。

    实际代码中,将51399个候选位置根据得分排序,选择最高的一部分,再经过Non-Maximum Suppression获得2000个候选结果。之后才送入分类器和回归器。
    所以Faster-RCNN和RCNN, Fast-RCNN一样,属于2-stage的检测算法。

    区域生成网络:训练

    样本

    考察训练集中的每张图像:
    a. 对每个标定的真值候选区域,与其重叠比例最大的anchor记为前景样本
    b. 对a)剩余的anchor,如果其与某个标定重叠比例大于0.7,记为前景样本;如果其与任意一个标定的重叠比例都小于0.3,记为背景样本
    c. 对a),b)剩余的anchor,弃去不用。
    d. 跨越图像边界的anchor弃去不用

    代价函数

    同时最小化两种代价:
    a. 分类误差
    b. 前景样本的窗口位置偏差
    具体参看fast RCNN中的“分类与位置调整”段落

    超参数

    原始特征提取网络使用ImageNet的分类样本初始化,其余新增层随机初始化。
    每个mini-batch包含从一张图像中提取的256个anchor,前景背景样本1:1.
    前60K迭代,学习率0.001,后20K迭代,学习率0.0001。
    momentum设置为0.9,weight decay设置为0.0005。[4]

    共享特征

    区域生成网络(RPN)和fast RCNN都需要一个原始特征提取网络(下图灰色方框)。这个网络使用ImageNet的分类库得到初始参数W0W_0,但要如何精调参数,使其同时满足两方的需求呢?本文讲解了三种方法。
    这里写图片描述

    轮流训练

    a. 从W0W_0开始,训练RPN。用RPN提取训练集上的候选区域
    b. 从W0W_0开始,用候选区域训练Fast RCNN,参数记为W1W_1
    c. 从W1W_1开始,训练RPN…
    具体操作时,仅执行两次迭代,并在训练时冻结了部分层。论文中的实验使用此方法。
    如Ross Girshick在ICCV 15年的讲座Training R-CNNs of various velocities中所述,采用此方法没有什么根本原因,主要是因为”实现问题,以及截稿日期“。

    近似联合训练

    直接在上图结构上训练。在backward计算梯度时,把提取的ROI区域当做固定值看待;在backward更新参数时,来自RPN和来自Fast RCNN的增量合并输入原始特征提取层。
    此方法和前方法效果类似,但能将训练时间减少20%-25%。公布的python代码中包含此方法。

    联合训练

    直接在上图结构上训练。但在backward计算梯度时,要考虑ROI区域的变化的影响。推导超出本文范畴,请参看15年NIP论文[5]。

    实验

    除了开篇提到的基本性能外,还有一些值得注意的结论

    • 与Selective Search方法(黑)相比,当每张图生成的候选区域从2000减少到300时,本文RPN方法(红蓝)的召回率下降不大。说明RPN方法的目的性更明确
      这里写图片描述

    • 使用更大的Microsoft COCO库[6]训练,直接在PASCAL VOC上测试,准确率提升6%。说明faster RCNN迁移性良好,没有over fitting。
      这里写图片描述


    1. Girshick, Ross, et al. “Rich feature hierarchies for accurate object detection and semantic segmentation.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2014. ↩︎

    2. Girshick, Ross. “Fast r-cnn.” Proceedings of the IEEE International Conference on Computer Vision. 2015. ↩︎

    3. M. D. Zeiler and R. Fergus, “Visualizing and understanding convolutional neural networks,” in European Conference on Computer Vision (ECCV), 2014. ↩︎

    4. learning rate-控制增量和梯度之间的关系;momentum-保持前次迭代的增量;weight decay-每次迭代缩小参数,相当于正则化。 ↩︎

    5. Jaderberg et al. “Spatial Transformer Networks”
      NIPS 2015 ↩︎

    6. 30万+图像,80类检测库。参看http://mscoco.org/。 ↩︎

    展开全文
  • 目标检测】RCNN算法详解

    万次阅读 多人点赞 2016-04-05 23:10:36
    深度学习用于目标检测的RCNN算法

    Girshick, Ross, et al. “Rich feature hierarchies for accurate object detection and semantic segmentation.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2014.

    Region CNN(RCNN)可以说是利用深度学习进行目标检测的开山之作。作者Ross Girshick多次在PASCAL VOC的目标检测竞赛中折桂,2010年更带领团队获得终身成就奖,如今供职于Facebook旗下的FAIR。
    这篇文章思路简洁,在DPM方法多年平台期后,效果提高显著。包括本文在内的一系列目标检测算法:RCNN, Fast RCNN, Faster RCNN代表当下目标检测的前沿水平,在github都给出了基于Caffe的源码

    思想

    本文解决了目标检测中的两个关键问题。

    问题一:速度

    经典的目标检测算法使用滑动窗法依次判断所有可能的区域。本文则预先提取一系列较可能是物体的候选区域,之后仅在这些候选区域上提取特征,进行判断。

    问题二:训练集

    经典的目标检测算法在区域中提取人工设定的特征(Haar,HOG)。本文则需要训练深度网络进行特征提取。可供使用的有两个数据库:
    一个较大的识别库(ImageNet ILSVC 2012):标定每张图片中物体的类别。一千万图像,1000类。
    一个较小的检测库(PASCAL VOC 2007):标定每张图片中,物体的类别和位置。一万图像,20类。
    本文使用识别库进行预训练,而后用检测库调优参数。最后在检测库上评测。

    流程

    RCNN算法分为4个步骤

    • 一张图像生成1K~2K个候选区域
    • 对每个候选区域,使用深度网络提取特征
    • 特征送入每一类的SVM 分类器,判别是否属于该类
    • 使用回归器精细修正候选框位置
      这里写图片描述

    候选区域生成

    使用了Selective Search1方法从一张图像生成约2000-3000个候选区域。基本思路如下:

    • 使用一种过分割手段,将图像分割成小区域
    • 查看现有小区域,合并可能性最高的两个区域。重复直到整张图像合并成一个区域位置
    • 输出所有曾经存在过的区域,所谓候选区域

    候选区域生成和后续步骤相对独立,实际可以使用任意算法进行。

    合并规则

    优先合并以下四种区域:

    • 颜色(颜色直方图)相近的
    • 纹理(梯度直方图)相近的
    • 合并后总面积小的
    • 合并后,总面积在其BBOX中所占比例大的

    第三条,保证合并操作的尺度较为均匀,避免一个大区域陆续“吃掉”其他小区域。

    例:设有区域a-b-c-d-e-f-g-h。较好的合并方式是:ab-cd-ef-gh -> abcd-efgh -> abcdefgh。
    不好的合并方法是:ab-c-d-e-f-g-h ->abcd-e-f-g-h ->abcdef-gh -> abcdefgh。

    第四条,保证合并后形状规则。

    例:左图适于合并,右图不适于合并。
    这里写图片描述

    上述四条规则只涉及区域的颜色直方图、纹理直方图、面积和位置。合并后的区域特征可以直接由子区域特征计算而来,速度较快。

    多样化与后处理

    为尽可能不遗漏候选区域,上述操作在多个颜色空间中同时进行(RGB,HSV,Lab等)。在一个颜色空间中,使用上述四条规则的不同组合进行合并。所有颜色空间与所有规则的全部结果,在去除重复后,都作为候选区域输出。

    作者提供了Selective Search的源码,内含较多.p文件和.mex文件,难以细查具体实现。

    特征提取

    预处理

    使用深度网络提取特征之前,首先把候选区域归一化成同一尺寸227×227。
    此处有一些细节可做变化:外扩的尺寸大小,形变时是否保持原比例,对框外区域直接截取还是补灰。会轻微影响性能。

    预训练

    网络结构
    基本借鉴Hinton 2012年在Image Net上的分类网络2,略作简化3
    这里写图片描述
    此网络提取的特征为4096维,之后送入一个4096->1000的全连接(fc)层进行分类。
    学习率0.01。

    训练数据
    使用ILVCR 2012的全部数据进行训练,输入一张图片,输出1000维的类别标号。

    调优训练

    网络结构
    同样使用上述网络,最后一层换成4096->21的全连接网络。
    学习率0.001,每一个batch包含32个正样本(属于20类)和96个背景。

    训练数据
    使用PASCAL VOC 2007的训练集,输入一张图片,输出21维的类别标号,表示20类+背景。
    考察一个候选框和当前图像上所有标定框重叠面积最大的一个。如果重叠比例大于0.5,则认为此候选框为此标定的类别;否则认为此候选框为背景。

    类别判断

    分类器
    对每一类目标,使用一个线性SVM二类分类器进行判别。输入为深度网络输出的4096维特征,输出是否属于此类。
    由于负样本很多,使用hard negative mining方法。
    正样本
    本类的真值标定框。
    负样本
    考察每一个候选框,如果和本类所有标定框的重叠都小于0.3,认定其为负样本

    位置精修

    目标检测问题的衡量标准是重叠面积:许多看似准确的检测结果,往往因为候选框不够准确,重叠面积很小。故需要一个位置精修步骤。
    回归器
    对每一类目标,使用一个线性脊回归器进行精修。正则项λ=10000\lambda=10000
    输入为深度网络pool5层的4096维特征,输出为xy方向的缩放和平移。
    训练样本
    判定为本类的候选框中,和真值重叠面积大于0.6的候选框。

    结果

    论文发表的2014年,DPM已经进入瓶颈期,即使使用复杂的特征和结构得到的提升也十分有限。本文将深度学习引入检测领域,一举将PASCAL VOC上的检测率从35.1%提升到53.7%
    本文的前两个步骤(候选区域提取+特征提取)与待检测类别无关,可以在不同类之间共用。这两步在GPU上约需13秒。
    同时检测多类时,需要倍增的只有后两步骤(判别+精修),都是简单的线性运算,速度很快。这两步对于100K类别只需10秒。

    以本论文为基础,后续的fast RCNN4(参看这篇博客)和faster RCNN5(参看这篇博客)在速度上有突飞猛进的发展,基本解决了PASCAL VOC上的目标检测问题。


    1. J. Uijlings, K. van de Sande, T. Gevers, and A. Smeulders. Selective search for object recognition. IJCV, 2013. ↩︎

    2. A. Krizhevsky, I. Sutskever, and G. Hinton. ImageNet classification with deep convolutional neural networks. In NIPS, 2012 ↩︎

    3. 所有层都是串行的。relu层为in-place操作,偏左绘制。 ↩︎

    4. Girshick, Ross. “Fast r-cnn.” Proceedings of the IEEE International Conference on Computer Vision. 2015. ↩︎

    5. Ren, Shaoqing, et al. “Faster R-CNN: Towards real-time object detection with region proposal networks.” Advances in Neural Information Processing Systems. 2015. ↩︎

    展开全文
  • 目标检测】Fast RCNN算法详解

    万次阅读 多人点赞 2016-04-12 13:05:42
    继2014年的RCNN之后,Ross Girshick在15年推出Fast RCNN,构思精巧,流程更为紧凑,大幅提升了目标检测的速度。

    Girshick, Ross. “Fast r-cnn.” Proceedings of the IEEE International Conference on Computer Vision. 2015.

    继2014年的RCNN之后,Ross Girshick在15年推出Fast RCNN,构思精巧,流程更为紧凑,大幅提升了目标检测的速度。在Github上提供了源码

    同样使用最大规模的网络,Fast RCNN和RCNN相比,训练时间从84小时减少为9.5小时,测试时间从47秒减少为0.32秒。在PASCAL VOC 2007上的准确率相差无几,约在66%-67%之间.

    思想

    基础:RCNN

    简单来说,RCNN使用以下四步实现目标检测:
    a. 在图像中确定约1000-2000个候选框
    b. 对于每个候选框内图像块,使用深度网络提取特征
    c. 对候选框中提取出的特征,使用分类器判别是否属于一个特定类
    d. 对于属于某一特征的候选框,用回归器进一步调整其位置
    更多细节可以参看这篇博客

    改进:Fast RCNN

    Fast RCNN方法解决了RCNN方法三个问题:

    问题一:测试时速度慢
    RCNN一张图像内候选框之间大量重叠,提取特征操作冗余。
    本文将整张图像归一化后直接送入深度网络。在邻接时,才加入候选框信息,在末尾的少数几层处理每个候选框。

    问题二:训练时速度慢
    原因同上。
    在训练时,本文先将一张图像送入网络,紧接着送入从这幅图像上提取出的候选区域。这些候选区域的前几层特征不需要再重复计算。

    问题三:训练所需空间大
    RCNN中独立的分类器和回归器需要大量特征作为训练样本。
    本文把类别判断和位置精调统一用深度网络实现,不再需要额外存储。

    以下按次序介绍三个问题对应的解决方法。

    特征提取网络

    基本结构

    图像归一化为224×224直接送入网络。

    前五阶段是基础的conv+relu+pooling形式,在第五阶段结尾,输入P个候选区域(图像序号×1+几何位置×4,序号用于训练)?。
    这里写图片描述

    注:文中给出了大中小三种网络,此处示出最大的一种。三种网络基本结构相似,仅conv+relu层数有差别,或者增删了norm层。

    roi_pool层的测试(forward)

    roi_pool层将每个候选区域均匀分成M×N块,对每块进行max pooling。将特征图上大小不一的候选区域转变为大小统一的数据,送入下一层。
    这里写图片描述

    roi_pool层的训练(backward)

    首先考虑普通max pooling层。设xix_ixi为输入层的节点,yjy_jyj为输出层的节点。
    ∂L∂xi={0δ(i,j)=false∂L∂yjδ(i,j)=true\frac{\partial L}{\partial x_i}=\begin{cases}0&\delta(i,j)=false\\ \frac{\partial L}{\partial y_j} & \delta(i,j)=true\end{cases}xiL={0yjLδ(i,j)=falseδ(i,j)=true

    其中判决函数δ(i,j)\delta(i,j)δ(i,j)表示i节点是否被j节点选为最大值输出。不被选中有两种可能:xix_ixi不在yjy_jyj范围内,或者xix_ixi不是最大值。

    对于roi max pooling,一个输入节点可能和多个输出节点相连。设xix_ixi为输入层的节点,yrjy_{rj}yrj为第rrr个候选区域的第jjj个输出节点。
    这里写图片描述
    ∂L∂xi=Σr,jδ(i,r,j)∂L∂yrj\frac{\partial L}{\partial x_i}=\Sigma_{r,j}\delta(i,r,j)\frac{\partial L}{\partial y_{rj}}xiL=Σr,jδ(i,r,j)yrjL

    判决函数δ(i,r,j)\delta(i,r,j)δ(i,r,j)表示i节点是否被候选区域r的第j个节点选为最大值输出。代价对于xix_ixi的梯度等于所有相关的后一层梯度之和。

    网络参数训练

    参数初始化

    网络除去末尾部分如下图,在ImageNet上训练1000类分类器。结果参数作为相应层的初始化参数。
    这里写图片描述
    其余参数随机初始化。

    分层数据

    在调优训练时,每一个mini-batch中首先加入N张完整图片,而后加入从N张图片中选取的R个候选框。这R个候选框可以复用N张图片前5个阶段的网络特征。
    实际选择N=2, R=128。

    训练数据构成

    N张完整图片以50%概率水平翻转。
    R个候选框的构成方式如下:

    类别 比例 方式
    前景 25% 与某个真值重叠在[0.5,1]的候选框
    背景 75% 与真值重叠的最大值在[0.1,0.5)的候选框

    分类与位置调整

    数据结构

    第五阶段的特征输入到两个并行的全连层中(称为multi-task)。
    这里写图片描述
    cls_score层用于分类,输出K+1维数组ppp,表示属于K类和背景的概率。
    bbox_prdict层用于调整候选区域位置,输出4*K维数组ttt,表示分别属于K类时,应该平移缩放的参数。

    代价函数

    loss_cls层评估分类代价。由真实分类uuu对应的概率决定:
    Lcls=−log⁡puL_{cls}=-\log p_uLcls=logpu

    loss_bbox评估检测框定位代价。比较真实分类对应的预测参数tut^utu和真实平移缩放参数为vvv的差别:
    Lloc=Σi=14g(tiu−vi)L_{loc}=\Sigma_{i=1}^4 g(t^u_i-v_i)Lloc=Σi=14g(tiuvi)
    g为Smooth L1误差,对outlier不敏感:
    g(x)={0.5x2∣x∣<1∣x∣−0.5otherwiseg(x)=\begin{cases} 0.5x^2& |x|<1\\|x|-0.5&otherwise \end{cases} g(x)={0.5x2x0.5x<1otherwise

    总代价为两者加权和,如果分类为背景则不考虑定位代价:
    L={Lcls+λLlocu为前景Lclsu为背景L=\begin{cases} L_{cls}+\lambda L_{loc}& u为前景\\ L_{cls} &u为背景\end{cases}L={Lcls+λLlocLclsuu

    源码中bbox_loss_weights用于标记每一个bbox是否属于某一个类

    全连接层提速

    分类和位置调整都是通过全连接层(fc)实现的,设前一级数据为xxx后一级为yyy,全连接层参数为WWW,尺寸u×vu\times vu×v。一次前向传播(forward)即为:
    y=Wxy=Wxy=Wx
    计算复杂度为u×vu\times vu×v

    WWW进行SVD分解,并用前t个特征值近似:
    W=UΣVT≈U(:,1:t)⋅Σ(1:t,1:t)⋅V(:,1:t)TW=U\Sigma V^T\approx U(:,1:t) \cdot \Sigma(1:t,1:t) \cdot V(:,1:t)^TW=UΣVTU(:,1:t)Σ(1:t,1:t)V(:,1:t)T

    原来的前向传播分解成两步:
    y=Wx=U⋅(Σ⋅VT)⋅x=U⋅zy=Wx = U\cdot (\Sigma \cdot V^T) \cdot x = U \cdot zy=Wx=U(ΣVT)x=Uz
    计算复杂度变为u×t+v×tu\times t+v \times tu×t+v×t
    在实现时,相当于把一个全连接层拆分成两个,中间以一个低维数据相连。
    这里写图片描述

    在github的源码中,这部分似乎没有实现。

    实验与结论

    实验过程不再详述,只记录结论

    • 网络末端同步训练的分类和位置调整,提升准确度
    • 使用多尺度的图像金字塔,性能几乎没有提高
    • 倍增训练数据,能够有2%-3%的准确度提升
    • 网络直接输出各类概率(softmax),比SVM分类器性能略好
    • 更多候选窗不能提升性能

    同年作者团队又推出了Faster RCNN,进一步把检测速度提高到准实时,可以参看这篇博客
    关于RCNN, Fast RCNN, Faster RCNN这一系列目标检测算法,可以进一步参考作者在15年ICCV上的讲座Training R-CNNs of various velocities

    展开全文
  • 目标检测

    万次阅读 2019-02-26 19:22:31
    文章目录第八章 目标检测8.1 基本概念8.1.1 什么是目标检测?8.1.2 目标检测要解决的核心问题?8.1.2 目标检测算法分类?8.2 Two Stage目标检测算法8.2.1 R-CNN8.2.2 Fast R-CNN8.2.3 Faster R-CNN8.2.4 R-FCN8.2.5 ...
  • 目标检测(一)——目标检测综述(持续更新中)

    万次阅读 多人点赞 2018-05-09 09:56:09
    目标检测 目标检测要解决的核心问题 目标检测最新进展 目标检测应用
  • 目标检测综述

    万次阅读 多人点赞 2019-07-09 21:36:26
    这意味着,我们不仅要用算法判断图片中是不是一辆汽车, 还要在图片中标记出它的位置, 用边框或红色方框把汽车圈起来, 这就是目标检测问题。 其中“定位”的意思是判断汽车在图片中的具体位置。 近几年来,目标...
  • 睿智的目标检测20——利用mAP计算目标检测精确度

    万次阅读 多人点赞 2020-03-06 17:12:48
    睿智的目标检测20——利用mAP计算目标检测精确度学习前言GITHUB代码下载知识储备1、IOU的概念2、TP TN FP FN的概念3、precision(精确度)和recall(召回率)4、概念举例5、单个指标的局限性什么是AP绘制mAP ...
  • 目标检测:YOLOv3: 训练自己的数据

    万次阅读 多人点赞 2018-03-26 10:59:02
    (B)关于cfg修改,以6类目标检测为例,主要有以下几处调整(蓝色标出),也可参考我上传的文件,里面对应的是4类。#表示注释,根据训练和测试,自行修改。 [net] # Testing # batch=1 # subdivisions=1 # Training ...
  • 睿智的目标检测18——Keras搭建FasterRCNN目标检测平台学习前言什么是FasterRCNN目标检测算法源码下载Faster-RCNN实现思路一、预测部分1、主干网络介绍2、从特征获取预测结果3、预测结果的解码4、在原图上进行绘制二...
  • 目标检测之 IoU

    万次阅读 多人点赞 2018-09-24 15:09:07
    IoU 作为目标检测算法性能 mAP 计算的一个非常重要的函数。 但纵观 IoU 计算的介绍知识,都是直接给出代码,给出计算方法,没有人彻底地分析过其中的逻辑,故本人书写该篇博客来介绍下其中的逻辑。 IoU 的全称为交...
  • 睿智的目标检测27——Pytorch搭建Faster R-CNN目标检测平台学习前言什么是FasterRCNN目标检测算法源码下载Faster-RCNN实现思路一、预测部分1、主干网络介绍2、获得Proposal建议框3、Proposal建议框的解码4、对...
  • 睿智的目标检测32——TF2搭建YoloV4目标检测平台(tensorflow2)学习前言什么是YOLOV4代码下载YOLOV4改进的部分(不完全)YOLOV4结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行...
  • 目标检测:传统目标检测方法

    万次阅读 多人点赞 2018-11-17 06:12:36
    文章目录@[toc]引言什么是传统目标检测?区域选择特征提取分类器传统目标检测方法不足 引言 提到Computer Vision,可能我们会最先想到CV的基本任务Image Classification,但在此基础上,还有其他更为复杂和有趣的...
  • 一文读懂目标检测:R-CNN、Fast R-CNN、Faster R-CNN、YOLO、SSD   前言 之前我所在的公司七月在线开设的深度学习等一系列课程经常会讲目标检测,包括R-CNN、Fast R-CNN、Faster R-CNN,但一直没有比较好的...
  • 人脸人眼目标检测

    千次下载 热门讨论 2011-11-16 10:58:16
    基于opencv2.3.1实现的人脸目标检测,利用了基于Haar特征级联分类器,效果很好
  • 目标检测——目标检测方法的综述

    千次阅读 2019-01-14 18:15:40
    一、目标检测问题的定义 1、定义: 目标检测时图片中对可变数量的目标进行查找和分类。 2、需要解决的问题 目标种类与数量问 目标尺度问题:目标稠密、目标大小等 外在环境干扰问题:遮挡、光照  二、目标检测...
  • 目标检测目标检测的目标是确定某张给定图像中是否存在给定类别(比如人、车、自行车、狗和猫)的目标实例;如果存在,就返回每个目标实例的空间位置和覆盖范围(比如返回一个边界框)。 目标检测的意义 作为图像...
  • 目标检测之小目标检测和遮挡问题

    千次阅读 2020-08-11 20:07:06
    目标检测trick 小目标难检测原因 小目标在原图中尺寸比较小,通用目标检测模型中,一般的基础骨干神经网络(VGG系列和Resnet系列)都有几次下采样处理: 导致小目标在特征图的尺寸基本上只有个位数的像素大小,...
  • 睿智的目标检测35——Pytorch 搭建YoloV4-Tiny目标检测平台学习前言什么是YOLOV4-Tiny代码下载YoloV4-Tiny结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行预测4、预测结果的解码5...
  • 目标检测】2020年遥感图像目标检测综述

    千次阅读 多人点赞 2020-08-17 12:44:16
    目标检测】2020年遥感图像目标检测综述本文禁止转载1. DOTA数据集:2. 待解决的问题:2.1 目标小而密集:2.2 任意旋转角:2.3 Anchor匹配问题:2.4 回归任务的边界问题:2.5 实例级噪声:3. ...
  • 目标检测算法之SSD

    万次阅读 多人点赞 2018-04-06 15:17:33
    码字不易,欢迎给个赞! 欢迎交流与转载,文章会同步发布在公众号:机器学习算法...目标检测近年来已经取得了很重要的进展,主流的算法主要分为两个类型:(1)two-stage方法,如R-CNN系算法,其主要思路是先通过启...
  • 睿智的目标检测21——如何调用摄像头进行目标检测学习前言GITHUB代码下载使用到的库实现思路实现代码 学习前言 好多人都想了解一下如何对摄像头进行调用,然后进行目标检测,于是我做了这个小BLOG。 GITHUB代码下载...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,476
精华内容 18,990
关键字:

目标检测