当前位置:   article > 正文

YOLOv3详解

yolov3


前言

yolo算法是一种one-stage目标检测算法,与two-stage目标检测算法最大区别在于运算速度上,YOLO系列算法将图片划分成若干个网格,再基于anchor机制生成先验框,只用一步就生成检测框,这种方法大大提升了算法的预测速度,今天我们主要学习的是YOLOv3算法的主要实现过程,YOLOv3的论文于2018年发表在CVPR上.

​​论文名称:YOLOv3: An Incremental Improvement

论文下载地址: https://arxiv.org/abs/1804.02767

一、YOLOv3网络模型分析

YOLOv3网络结构大致可分为三个部分:BackbonePANetYolo Head,在YOLOv3-SPP结构中还引入了SPP结构,但与SPPnet的SPP略有不同。
yolov3网络结构图

1.Backbone: Darknet-53

首先了解一下yolov3backbone部分,Darknet-53主体与resnet结构相似,堆叠了多个 残差模块,残差模块之间间隔了一个kernel_size=3x3stride=2卷积层,作用主要是downsample,53代表了整个backbone一共有52个卷积层和最后的connect层(全连接层),一共53层结构,下图是以输入图像256 x 256进行预训练来进行介绍的,常用的尺寸是416 x 416,都是32的倍数(经过stride后会变换特征图大小一共有5次,每次都是2倍所以确定img_size大小要是 2 5 = 32 2^5=32 25=32)。
卷积的strides默认为1padding默认为same(补全),当strides2时padding为valid(丢弃).
在这里插入图片描述
第一个3x3的卷积核主要用来增加通道数,在不改变图片尺寸的条件下获得更多的有效特征图,同时扩大特征图感受野,第二个3x3的卷积核stride=2主要作用是downsample,减少计算过程中的参数量与计算量,再堆叠多个residual block,最后对得到的feature map进行平均池化。
以上的Residual模块的残差连接方式与Resnet基本相同,输入特征图与输出特征图中间经过卷积核大小为1x13x3大小的两次卷积,再将输出的两个特征图add,Convolutional模块=conv+bn+leakrelu,如下图所示:
Residual and Convlutional

2.FPN

FPN特征金字塔融合是一种自下而上,自上而下,横向连接和卷积融合。
自下而上指图像通过卷积降维和提取特征的过程(特征图尺寸从大到小),自上而下(小到大)通过上采样与特征融合的方法融合多级特征图的不同语义信息。这样既拥有了高层的语义信息,又拥用了底层的轮廓信息,FPN是add,这里是concatenate。
在这里插入图片描述

3.Yolo Head

Yolo Head是一种decoder,它的主要结构是一个conv+bn+act模块与一个kernel_size =1x1卷积分类层,用1x1卷积代替全连接层进行分类的主要原因有两点:
(1) 全连接层输入尺度被限定,而卷积层只用限定输入和输出channel
(2)全连接层输出是一维或二维,特征图输入到全连接层时需要对其进行Flatten,这样在一定程度上破环了特征图的空间信息,而卷积层输出为三维(c,w,h),选择卷积层作为decoder,极大保留了特征图上对应原图的空间结构信息,如下图所示:
更便于匹配到正样本时,输出空间上对应的channel的值.
yolov3

二、anchor网格偏移量预测

由前一部分的讲解可知,Yolo Head输出是三维的w,h 的每一个点对应原图划分的网格,channel对应的是该anchor的预测值,那我们如何计算anchor的具体位置呢?
yolo的思想是将一张图片划分为W*W个网格,每一个网格负责中心点落在该网格的目标.每个网格可以看作一个感兴趣区域,既然是区域,就需要计算预测anchor的具体坐标与bboxwh.
c h a n n e l = t x + t y + t w + t h + o b j + n u m c l a s s e s channel = t_x+t_y+t_w+t_h+obj+num_{classes} channel=tx+ty+tw+th+obj+numclasses
其中预测值tx,ty并不是anchor的坐标,而是anchor的偏移量,同样的 t w , t h t_w,t_h tw,th是先验框的缩放因子,先验框的大小由k-means聚类xml标签文件中保存的坐标位置得到,每一个anchor有三个不同大小的先验框,预测层Yolohead有三个,总共的先验框数量为划分总网格数的三倍.
anchor 坐标预测
上图表示了bbox的回归过程 c x , c y c_x,c_y cx,cy是网格左上角坐标,anchor向右下方偏移,为了防止anchor偏移量超出网格导致定位精度损失过高,yolov3使用了sigmoid函数将预测值tx,ty进行限制到[0,1](可以加速网络的收敛),最后得出anchor的预测坐标 b x , b y b_x,b_y bx,by.bbox的w与h由预测缩放因子 t w 与 t h t_w与t_h twth决定, p w 和 p h p_w和p_h pwphanchor模板映射到特征图上的宽和高,通过指数函数对 t w t_w tw t h t_h th进行放大在分别与 p w 和 p h p_w和p_h pwph相乘就得到了最终预测的w和h.
PS:此处的x,y坐标点都是anchor映射到特征图的值,并不是原图上的坐标,在plot bbox应该将其转换到真实图片上的坐标再进行绘制.

三、正负样本匹配规则

yolov3论文中提到正负样本的匹配规则是:给每一个groundtrue box分配一个正样本,这个正样本是所有bbox中找一个与gt_box的重叠区域最大的一个预测框,也就是和该gt_box的iou最大的预测框.但是如果利用这个规则去寻找正样本,正样本的数量是很少的,这将使得网络难以训练.如果一个样本不是正样本,那么它既没有定位损失,也没有类别损失,只有置信度损失,在yolov3的论文中作者尝试用focal loss来缓解正负样本不均匀的问题,但是并没有取得很好的效果,原因就在于负样本值参与了置信度损失,对loss的影响占比很小.
所以我们要去看作者在源码中是如何匹配正样本的.
源码中定义了build_targets函数来匹配正样本.

def build_targets(p, targets, model):
    """
    Build targets for compute_loss(), input targets(image,class,x,y,w,h)
    :param p: 预测框 由模型构建中的yolo_out返回的三个yolo层的输出
              tensor格式 list列表 存放三个tensor 对应的是三个yolo层的输出
              例如:[4, 3, 23, 23, 25]  [4, 3, 46, 46, 25]  [4, 3, 96, 96, 25]  (736x736尺度下)
              [batch_size, anchor_num, grid, grid, xywh + obj + classes]
              p[i].shape
    :param targets: 数据增强后一个batch的真实框 [21, 6] 21: num_object  6: batch中第几张图(0,1,2,3)+类别+x+y+w+h真实框
    :param model: 初始化的模型
    :return: tbox: append [m(正样本个数), x偏移量(中心点坐标相对中心所在grid_cell左上角的偏移量) + y偏移量 + w + h]
                   存放着当前batch中所有anchor的正样本 某个anchor的正样本指的是当前的target由这个anchor预测
                   另外,同一个target可能由多个anchor预测,所以通常 m>nt
             indices: append [m(正样本个数), b + a + gj + gi]
                   b: 和tbox一一对应 存放着tbox中对应位置的target(第a个anchor的正样本)属于这个batch中的哪一张图片
                   a: 和tbox一一对应 存放着tbox中对应位置的target是属于哪个anchor(index)的正样本(由哪个anchor负责预测)
                   gj: 和tbox一一对应 存放着tbox中对应位置的target的中心点所在grid_cell的左上角的y坐标
                   gi: 和tbox一一对应 存放着tbox中对应位置的target(第a个anchor的正样本)的中心点所在grid_cell的左上角的x坐标
             tcls: append [m] 和tbox一一对应 存放着tbox中对应位置的target(第a个anchor的正样本)所属的类别
             anch: append [m, 2] 和tbox一一对应 存放着tbox中对应位置的target是属于哪个anchor(shape)的正样本(由哪个anchor负责预测)
    """

    nt = targets.shape[0]  # 当前batch真实框的数量 num of target

    # 定义一些变量
    # anch append [m, 2] 和tbox一一对应 存放着对应位置的target是属于哪个anchor(shape)的正样本(由哪个anchor负责预测)
    tcls, tbox, indices, anch = [], [], [], []
    gain = torch.ones(6, device=targets.device)  # normalized to gridspace gain tensor([1,1,1,1,1,1])

    multi_gpu = type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)  # 一般False
    for i, j in enumerate(model.yolo_layers):  # [89, 101, 113]  i,j = 0, 89   1, 101   2, 113
        # 获取该yolo predictor对应的anchors的大小,不过这里是缩放到feature map上的  shape=[3, 2]
        # 而且它就是cfg文件中的anchors除以了缩放比例stride得到的
        # 比如:[3.6250, 2.8125] / 13 * 416 (缩放比例为32)= [116,90]  等于cfg文件中的anchor的大小
        anchors = model.module.module_list[j].anchor_vec if multi_gpu else model.module_list[j].anchor_vec

        # gain中存放的是feature map的尺寸信息
        # 在原图尺度为(736,736)情况下  p[0]=[4,3,23,23,25] p[1]=[4,3,46,46,25] p[2]=[4,3,92,92,25]
        # 如原图(736x736)  gain=Tensor([1,1,23,23,23,23]) 或 Tensor([1,1,46,46,46,46]) 或 Tensor([1,1,92,92,92,92])
        gain[2:] = torch.tensor(p[i].shape)[[3, 2, 3, 2]]  # xyxy gain

        na = anchors.shape[0]  # number of anchors  3个
        # [3] -> [3, 1] -> [3, nt]
        # anchor tensor, same as .repeat_interleave(nt)   at.shape=[3,21] 21个0, 1, 2
        at = torch.arange(na).view(na, 1).repeat(1, nt)

        # Match targets to anchors
        # t = targets * gain: 将box坐标(在box标签生成中,对box坐标进行了归一化,即除以图像的宽高)转换到当前yolo层输出的特征图上
        #                     通过将归一化的box乘以特征图尺度,从而将box坐标投影到特征图上
        # 广播原理 targets=[21,6]  gain=[6] => gain=[6,6] => t=[21,6]
        a, t, offsets = [], targets * gain, 0

        if nt:  # 如果存在target的话
            # 把yolo层的anchor在该feature map上对应的wh(anchors)和所有预测真实框在该feature map上对应的wh(t[4:6])做iou,
            #       若大于model.hyp['iou_t']=0.2, 则为正样本保留,否则则为负样本舍弃
            # anchors: [3, 2]: 当前yolO层的三个anchor(且都是相对416x416的, 不过初始框的wh多大都可以,反正最后都是做回归)
            # t[:, 4:6]: [nt, 2]: 所有target真是框的w和h, 且都是相对当前feature map的
            # j: [3, nt]
            j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t']  # iou(3,n) = wh_iou(anchors(3,2), gwh(n,2))
            # t.repeat(na, 1, 1): [nt, 6] -> [3, nt, 6]
            # 获取iou大于阈值的anchor与target对应信息
            # a=tensor[30]: anchor_index(0、1、2)  0表示是属于第一个anchor(包含4张图片)的正样本  同理第二。。。
            #               再解释下什么是正样本: 表示当前target可以由第i个anchor检测,就表示当前target是这个anchor的正样本
            # t=tensor[30,6]: 第0、1、2、3(4张图片)的target, class, x, y, w, h(相对于当前feature map尺度)
            #                 与a变量一一对应,a用来指示t中相对应的位置的target是属于哪一个anchor的正样本
            #                 注意:这里的同一个target是可能会同属于多个anchor的正样本的,由多个anchor计算同一个target
            #                      不然t个数也不会大于正样本数(30>21)
            a, t = at[j], t.repeat(na, 1, 1)[j]  # filter 选出所有anchor对应属于它们的正样本

        # Define
        # b: 对应图片的index 即当前target是属于哪张图片的
        # c: 当前target是属于哪个类
        # long等于to(torch.int64), 数值向下取整  这里都是整数,long()只起到了float->int的作用
        b, c = t[:, :2].long().T  # image, class

        gxy = t[:, 2:4]  # grid xy  对应于当前feature map的target的xy坐标
        gwh = t[:, 4:6]  # grid wh  对应于当前feature map的target的wh坐标

        # 匹配targets所在的grid cell左上角坐标
        # (gxy-0).long 向下取整  得到当前target的中心点所在左上角的坐标
        gij = (gxy - offsets).long()
        # grid xy indices 左上角x, y坐标
        gi, gj = gij.T

        # Append
        indices.append((b, a, gj, gi))  # image index, anchor, grid indices(x, y)
        tbox.append(torch.cat((gxy - gij, gwh), 1))  # gt box相对当前feature map的x,y偏移量以及w,h
        anch.append(anchors[a])  # anchors
        tcls.append(c)  # class

        if c.shape[0]:  # if any targets
            # 目标的标签数值不能大于给定的目标类别数
            assert c.max() < model.nc, 'Model accepts %g classes labeled from 0-%g, however you labelled a class %g. ' \
                                       'See https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' % (
                                           model.nc, model.nc - 1, c.max())
    return tcls, tbox, indices, anch


# 这里是根据宽高(左上角对齐)来进行iou计算处理
# 与普通的IoU,GIoU,那些不一样
def wh_iou(wh1, wh2):
    """
    把yolo层的anchor在该feature map上对应的wh(anchors)和所有预测真实框在该feature map上对应的wh(t[4:6])做iou,
    若大于model.hyp['iou_t']=0.2, 则为正样本保留,否则则为负样本舍弃  筛选出符合该yolo层对应的正样本
    Args:
        wh1: anchors  [3, 2]
        wh2: target   [22,2]
    Returns:
        wh1 和 wh2 的iou [3, 22]
    """
    # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
    wh1 = wh1[:, None]  # [N,1,2]  [3, 1, 2]
    wh2 = wh2[None]  # [1,M,2]     [1, 22, 2]
    inter = torch.min(wh1, wh2).prod(2)  # [N,M]  [3, 22]
    return inter / (wh1.prod(2) + wh2.prod(2) - inter)  # iou = inter / (area1 + area2 - inter)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

通过对上面源码的阅读我们可以得出作者首先将bboxgr_box的左上角对齐,再计算出存在目标的anchorbboxgr_boxiou,并设定一个iou阈值,如果anchor templateiou大于阈值则归为正样本.

四、损失函数

损失分为三个部分:置信度损失,定位损失和类别损失.
L = L c l s + L c o n f + L l o c L = L_{cls} + L_{conf} +L_{loc} L=Lcls+Lconf+Lloc
在正负样本匹配部分我已经解释过只有正样本才有这三类损失,而负样本只有置信度损失,所以在损失计算的过程中我们需要将anchor template划分为正样本与负样本两种情况来计算loss.

1.类别损失(只考虑正样本)

L o s s c l s Loss_{cls} Losscls选用的是BCE损失函数:
L c l s = − ∑ i ∈ p o s ∑ j ∈ c l s ( O i j l n ( C ^ i j ) ) + ( 1 − O i j ) l n ( 1 − C ^ i j ) L_{cls} =-\sum_{i\in pos}\sum_{j\in cls}(O_{ij}ln(\hat C_{ij}))+(1-O_{ij})ln(1-\hat C_{ij}) Lcls=iposjcls(Oijln(C^ij))+(1Oij)ln(1C^ij)
C ^ i j = σ ( C i j ) \hat C_{ij} = \sigma (C_{ij}) C^ij=σ(Cij)
O i j O_{ij} Oij代表第i个正样本中的第j个类别是否存在,若存在则为1,不存在为0.(根据自己打的标签来)
C ^ i j \hat C_{ij} C^ij代表经过sigmoid函数分类后的预测值.
yolov3论文片段
上图是从原论文中截取的对分类损失函数的描述,作者的观点是获取好的分类性能并不一定要使用softmax,相反使用了使sigmoid函数分类和bce损失.
读到这儿,平时我们一般都使用softmax进行多分类,那为什么使用sigmoid进行分类?
independent logistic classifiers指的是sigmoid函数,个人的理解是:它的好处是将类别概率限制到[0,1]之间,且每个类别的概率值相互独立,互不影响,适用于多标签分类.softmax在进行分类时使所有类别概率总和为1,很多时候会出现“一家独大”的情况,即某一个类别占比较大,概率值较高,其他类别的概率值很低,在计算类别损失时无法达到一个好的效果,并不利于网络的收敛.
总而言之,softmax函数不适用于多标签分类,预测值通过sigmoid函数后每一类的值相互独立互不影响,适用于同时预测多类目标,即多标签分类.

2.置信度损失(考虑所有样本)

置信度 L o s s c o n f Loss_{conf} Lossconf计算公式如下所示:
L o s s c o n f = − ∑ i O i l n ( c ^ i ) + ( 1 − O i ) l n ( 1 − c ^ i ) Loss_{conf}=-\sum_iO_iln(\hat c_i)+(1-O_i)ln(1-\hat c_i) Lossconf=iOiln(c^i)+(1Oi)ln(1c^i)
C ^ i = σ ( C i ) \hat C_{i} = \sigma (C_{i}) C^i=σ(Ci)
O i O_i Oi代表预测目标边界框i中是否真实存在目标.若存在则为1,不存在为0.(根据自己打的标签来)
C ^ i \hat C_{i} C^i代表表示预测目标矩形框i内是否存在目标的Sigmoid概率.
Sigmoid函数作用主要是将预测值范围规定到[0,1]
yolov3论文片段
yolov3论文中提出confidence score的预测使用的是一种逻辑回归的方式,所以损失函数用的是BCE.

3.定位损失(只考虑正样本)

置信度 L o s s l o c Loss_{loc} Lossloc计算公式如下所示:
L o s s l o c = ∑ i ∈ p o s ( t ^ ∗ − t ∗ ) 2 Loss_{loc}=\sum_{i\in pos}(\hat t_*-t_*)^2 Lossloc=ipos(t^t)2
yolov3论文片段
根据以上论文内容可知,计算 L o s s l o c Loss_{loc} Lossloc时使用的时误差平方和公式, t ^ ∗ \hat t_* t^为真实值, t ∗ t_* t为预测值
所以 L o s s l o c Loss_{loc} Lossloc可写作如下形式
L o s s l o c ( x , y ) = ∑ i ∈ p o s ∑ m ∈ x , y [ ( g m − c m ) − σ ( t m ) ] 2 Loss_{loc (x,y)} = \sum_{i\in pos}\sum_{m\in{x,y}}[ (g_m-c_m)-\sigma(t_m)]^2 Lossloc(x,y)=iposmx,y[(gmcm)σ(tm)]2
其中 t ^ ∗ = g m − c m \hat t_* =g_m-c_m t^=gmcm t ∗ = σ ( t m ) t_* = \sigma(t_m) t=σ(tm)
L o s s l o c ( w , h ) = ∑ i ∈ p o s ∑ n ∈ w , h ( l n g m i p n i − t n ) 2 Loss_{loc (w,h)} = \sum_{i\in pos}\sum_{n\in{w,h}}(ln\frac{g_m^i}{p_n^i}-t_n)^2 Lossloc(w,h)=iposnw,h(lnpnigmitn)2
其中 t ^ ∗ = l n g m i p n i \hat t_* =ln\frac{g_m^i}{p_n^i} t^=lnpnigmi t ∗ = t n t_* = t_n t=tn
L o s s l o c = L o s s l o c ( x , y ) + L o s s l o c ( w , h ) Loss_{loc}=Loss_{loc (x,y)} +Loss_{loc (w,h)} Lossloc=Lossloc(x,y)+Lossloc(w,h)
yolov3中的定位损失采用SSE显然是不太合适的,SSE主要是对预测所得anchor中心偏移量与宽高缩放因子进行误差分析,是对点进行回归分析,并不能较好的反应预测框与真实框之间的误差关系,这个问题我们将在下一节yolov4的讲解中进行讨论.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/584956
推荐阅读
相关标签
  

闽ICP备14008679号