经典目标检测YOLO系列(三)YOLOv3算法详解

经典目标检测YOLO系列(三)YOLOv3算法详解

  • 不论是YOLOv1,还是YOLOv2,都有一个共同的致命缺陷:小目标检测的性能差。尽管YOLOv2使用了passthrough技术将16倍降采样的特征图(即C4特征图)融合到了C5特征图中,但最终的检测仍是在C5尺度的特征图上进行的。

  • 为了解决这一问题,YOLO作者做了第3次改进,主要改进如下:

    • 使用了更好的主干网络DarkNet-53
    • 使用了多级检测与特征金字塔FPN方法
    • 修改损失函数

1 YOLOv3的改进之处

1.1 更好的主干网络DarkNet-53

  • 下图是DarkNet-53的网络架构图。

  • 相较于YOLOv2中所使用的DarkNet19,新的网络使用了53层卷积。

  • 同时,添加了残差网络中的残差连结结构,以提升网络的性能。

  • DarkNet53网络中的降采样操作没有使用Maxpooling层,而是由stride=2的卷积来实现。

  • 卷积层仍旧是线性卷积、BN层以及LeakyReLU激活函数的串联组合。

  • 虚线框是核心模块,由一层1×1卷积和一层3×3卷积层串联构成的残差模块。

经典目标检测YOLO系列(三)YOLOv3算法详解_第1张图片

在ImageNet数据集上,DarkNet53的top1准确率和top5准确率几乎与ResNet101和ResNet152持平,但速度却显著高于后两者。因此,相较于所对比的两个残差网络,DarkNet53在速度和精度上具有更高的性价比。

不过DarkNet53没有成为学术界的主流模型,其受欢迎程度仍不及ResNet系列。

经典目标检测YOLO系列(三)YOLOv3算法详解_第2张图片

1.2 多级检测与特征金字塔

  • YOLO-V1模型精度不足的一个重要表现就是召回率低,即该检的检不出,这一缺点在针对小目标检测方面表现的尤为明显。
  • 为了提高目标检测的召回率,YOLO-V2通过使用passthrough操作将浅层特征与深层特征进行融合,使得最终用于目标预测的特征中,既包含细节信息也包含语义信息。
    • 这种操作在一定程度上提高了目标检测的召回率,针对小型目标的检测能力也有明显提高。
    • 然而,仅使用一层细节特征进行细节特征融合往往是不够的(SSD的目标预测在6个尺度的特征图上进行)。
    • 因此,YOLO-V3借助了特征金字塔网络(Feature Pyramid Network, FPN)机制,从3个不同尺度的融合特征上进行目标预测。FPN是2017年(早于YOLO-V3提出一年)提出的一种特征融合网络结构,旨在为目标检测模型提供一种有效的多尺度特征融合机制。

1.2.1 特征金字塔FPN

  • FPN工作认为网络浅层的特征图包含更多的细节信息,但语义信息较少,而深层的特征图则恰恰相反

  • 随着网络深度的加深,降采样操作的增多,细节信息不断被破坏,致使小物体的检测效果逐渐变差,而大目标由于像素较多,仅靠网络的前几层还不足以使得网络能够认识到大物体(感受野不充分),但随着层数变多,网络的感受野逐渐增大,网络对大目标的认识越来越充分,检测效果自然会更好。

  • 因此,用浅层网络负责检测较小的目标,深层网络负责检测较大的目标。实现这一技术路线的就是SSD网络,但SSD只关注了信息数量问题,没有关注语义深浅问题。浅层特征虽然保留足够多的位置信息,但是语义信息的层次较浅,对目标的理解和认识不够充分。

    经典目标检测YOLO系列(三)YOLOv3算法详解_第3张图片

  • 考虑识别物体的类别依赖于语义信息,因此FPN利用自顶向下(top-down)的特征融合结构,利用空间上采样将深层网络的语义信息融合到浅层网络中(下图中的d)。

    • 处于性能和算力之间的平衡考虑,我们只会使用到主干网络输出的3个尺度的特征图,即C3、C4和C5,其降采样倍数分别为8、16和32。FPN会通过1×1卷积、3×3卷积以及上采样操作得到P3、P4和P5特征图。
    • 如果我们输入图像比较大,如800×1333,C5特征图感受野就不够大,无法覆盖到一些大目标,而且自身的语义信息相对较浅,因此就会在C5或者P5进一步降采样得到特征图P6,甚至P7。例如,RetinaNet以及FCOS等。

    经典目标检测YOLO系列(三)YOLOv3算法详解_第4张图片

1.3 YOLOv3中的FPN

  • YOLOv3的关键改进便是使用了FPN结构与多级检测方法。YOLOv3在3个尺度上去进行预测,分别是经过8倍降采样的特征图C3、经过16倍降采样的特征图C4和经过32倍降采样的特征图C5。YOLOv3网络结构如下图所示。

    • YOLOv3中的FPN,特征融合采用通道拼接,而非求和。

    • YOLOv3中FPN的卷积层较多。

    • YOLOv3最终会输出52×52×3(1+C+4)、26×26×3(1+C+4)和13×13×3(1+C+4)三个预测张量,然后将这些预测结果汇总到一起,进行后处理,得到最终的检测结果。

  • 从网格角度来看,假如输入图像是416×416,那么DarkNet-53输出的3个特征图:C3(52×52×256)、C4(26×26×512)和C5(13×13×1024)。相当于针对输入图像做了不同疏密的网格,显然越密的网格越适合检测小物体,而越疏的网格越适合检测大物体。

  • 在每个特征图上,YOLOv3在每个网格处放置3个先验框。

    • 由于YOLOv3一共使用3个尺度,因此,YOLOv3一共设定了9个先验框,这9个先验框仍旧是使用kmeans聚类的方法获得的。

    • 在COCO上,这9个先验框的宽高分别是(10, 13)、(16, 30)、(33, 23)、(30, 61)、(62, 45)、(59, 119)、(116, 90)、(156, 198)、(373, 326)。

      • C3特征图,每个网格处放置(10, 13)、(16, 30)、(33, 23)三个先验框,用来检测较小的物体。
      • C4特征图,每个网格处放置(30, 61)、(62, 45)、(59, 119)三个先验框,用来检测中等大小的物体。
      • C5特征图,每个网格处放置(116, 90)、(156, 198)、(373, 326)三个先验框,用来检测较大的物体。
    • 可以使用下面代码可视化,这三组先验框。

      #!/usr/bin/env python
      # -*- coding:utf-8 -*-
      import os
      import cv2
      
      
      def show_anchor_box(picture_path, FEATURE_MAP_SIZE=13):
          # 输入图片尺寸
          INPUT_SIZE = 416
      
          # 在coco数据集上,利用kmeans聚类出来的9组不同宽高的anchor box
          mask52 = [0, 1, 2]
          mask26 = [3, 4, 5]
          mask13 = [6, 7, 8]
          anchors = [
              10, 13, 16, 30, 33, 23,  # 小物体
              30, 61, 62, 45, 59, 119,  # 中等物体
              116, 90, 156, 198, 373, 326  # 大物体
          ]
          GRID_SHOW_FLAG = True
      
      
          img = cv2.imread(picture_path)
          print("原始图片的shape: ", img.shape)
          img = cv2.resize(img, (INPUT_SIZE, INPUT_SIZE))
      
          # 显示网格,颜色为黑色
          if GRID_SHOW_FLAG:
              height, width, channels = img.shape
              GRID_SIZEX = int(INPUT_SIZE / FEATURE_MAP_SIZE)
              for x in range(0, width - 1, GRID_SIZEX):
                  cv2.line(img, pt1 = (x, 0), pt2 = (x, height), color = (0, 0, 0), thickness = 1, lineType = 1)  # x grid
      
              GRID_SIZEY = int(INPUT_SIZE / FEATURE_MAP_SIZE)
              for y in range(0, height - 1, GRID_SIZEY):
                  cv2.line(img, pt1 = (0, y), pt2 = (width, y),  color = (0, 0, 0), thickness = 1, lineType = 1)  # y grid
      
          if FEATURE_MAP_SIZE == 13:
              for ele in mask13:
                  # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色
                  # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2
                  cv2.rectangle(img,
                                    pt1 = ((int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2 + 1]))),
                                    pt2 = ((int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2 + 1]))),
                                    color = (0, 0, 255),
                                    thickness = 2
                            )
      
          if FEATURE_MAP_SIZE == 26:
              for ele in mask26:
                  # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色
                  # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2
                  cv2.rectangle(img,
                                    pt1 = ((int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2 + 1]))),
                                    pt2 = ((int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2 + 1]))),
                                    color = (0, 0, 255),
                                    thickness = 2
                            )
      
          if FEATURE_MAP_SIZE == 52:
              for ele in mask52:
                  # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色
                  # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2
                  cv2.rectangle(img,
                                    pt1 = ((int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 - 0.5 * anchors[ele * 2 + 1]))),
                                    pt2 = ((int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2]), int(INPUT_SIZE * 0.5 + 0.5 * anchors[ele * 2 + 1]))),
                                    color = (0, 0, 255),
                                    thickness = 2
                            )
      
      
          cv2.imshow('img', img)
          while cv2.waitKey(1000) != 27:  # loop if not get ESC.
              if cv2.getWindowProperty('img', cv2.WND_PROP_VISIBLE) <= 0:
                  break
          cv2.destroyAllWindows()
      
      
      if __name__ == '__main__':
          directory = './imgs'
          for filename in os.listdir(directory):
              picture_path = os.path.join(directory, filename)
              show_anchor_box(picture_path, FEATURE_MAP_SIZE=13)
              show_anchor_box(picture_path, FEATURE_MAP_SIZE=26)
              show_anchor_box(picture_path, FEATURE_MAP_SIZE=52)
      

1.3 修改损失函数

  • 边界框的置信度损失。
    • 由YOLOv1及YOLOv2的MSE损失函数,改为BCE损失,即我们之前自己实现的YOLOv1及YOLOv2中的损失函数。
    • 不设置正负样本的权重,尽管负样本数量远远大于正样本。
    • 不使用预测框和目标框的IoU值作为置信度的学习标签,而采用0/1离散值,即我们之前自己实现的YOLOv1做法。
  • 类别损失。
    • 不同于之前的MSE损失函数,YOLOv3先使用sigmoid函数将每个类别的置信度映射到0到1之间,再使用BCE去计算每个类别的损失,即我们之前自己实现的YOLOv1做法。
    • 不使用softmax的解释。
      • softmax面对的类别必须是平行互斥的,预测得到最终的类别取概率分布中的最大者。
      • 当面对类别标签为非平行互斥的数据集,softmax预测将无能为力。
      • 与之不同的是,sigmoid预测得到的结果仅表示属于对应类别可能性,与其他类别无关,预测类别之间不互斥在某种意义上意味着对象可以拥有多个标签。
  • 边界框损失
    • 使用BCE函数来计算中心点偏移量的损失
    • 使用MSE计算宽高偏移量的损失

1.4 YOLOv3效果

  • 相较于YOLOv2的APs指标5.0,YOLOv3达到了18.3,小目标检测能力大大提高。

  • 尽管YOLOv3的性能不及RetinaNet,但在AP50指标上,YOLOv3几乎和RetinaNet达到一个水准,但YOLOv3的速度是后者的3倍左右。

经典目标检测YOLO系列(三)YOLOv3算法详解_第5张图片

2 YOLOv3的复现

  • 事实上,YOLOv2最大的变化就在于使用了多级检测以及FPN。

  • 后面依然不会百分之百地复现官方的YOLOv3,先给出实现的网络结构图。

    经典目标检测YOLO系列(三)YOLOv3算法详解_第6张图片

你可能感兴趣的:(#,深度学习,目标检测,YOLO,python)