经典算法YOLOv3解析。

禁止转载,侵权必究

前言

系列教程中我们介绍了图形分类的经典算法。那么在实际生活中,经常碰到这样的需求,在图中找到物体并标出它的分类和位置。技术上应该怎么实现呢?

我们可以把这个问题分解为2步,第一步是找到物体在图像中的位置(ROI),第二步是图像分类。因此最早的目标检测算法由此产生,R-CNN、Fast R-CNN、Faster R-CNN、Mask R-CNN。这些算法统一归类为两阶段目标检测算法。

经过数据科学家不断探索,在两阶段算法基础上又出现了一阶段算法,也就是说同时完成位置定位(ROI)和图像分类,比如:SSD、 YOLO(v1、v2、v3)

YOLOv3算法原理

Ground Truth Box 真实框GTB

数据中人工标识的物体真实位置。(x1, y1, x2, y2)

Anchor Box 锚框AB

对图像均分,然后再每个小格画3个框,框的大小是预设的。简而言之,就是用超参数来画格子,以此格子为基础来算bounding box。

Bounding Box 边界框BB

Bounding box是算法目标,我们要通过算法,把基本的anchor box去逼近标注的ground truth box,得到包含物体的那些边界框,这些计算得到的边界框又叫预测框pred_box。

IoU

算法中计算边界框和真实框的覆盖程度,数值越大越好。

计算过程

  1. 把训练图划分成小方块,比如一张640×480的图片,按32×32尺寸大小,一共可以分成20×15个区域。
  2. 以每个小方块中心为AB的中心,生成超参数[w, h]指定的3个AB,一共生成20x15x3 = 900个AB
  3. 遍历GTB,转换为块坐标,比如第2行第10列,块坐标为[2, 10]
  4. 通过计算IoU找到GTB对应的AB,微调AB计算预测框[bx, by, bh, bw],让预测框逼近真实框
  5. 算法的设计者要求bx,by 不能离开划定的小方块,同时bh,bw不允许为负数。这个约束条件导致算法不好实现。好在算法设计者给我们提出了解决方案。通过计算一组参数[tx, ty, th, tw],只要求它们为实数即可。
  6. 经过上面步骤,问题转化为计算AB的标注向量:objectness(是否包含物体),[tx, ty, th, tw](AB和GTB重合时的位置参数),classification(分类标签)。
  7. 我们把和GTB计算所得IoU最大的AB的objectness标识为1,其他两个AB标识为0。(例外情况:如果某个AB的IoU>0.7 但不是此小方块中最大的AB,标识为-1)
  8. [tx, ty, th, tw]参数组需要满足让此AB和GTB重合,可表示为下面四个代数公式:
  9. tx = gtx – cx (tx是sigmoid函数)
  10. ty = gty- cy (ty是sigmoid函数)
  11. tw = log( gtw/pw ) (log为自然对数e)
  12. th = log( gth/ph ) (log为自然对数e)
  13. classification(类型标签)采用了one-hot向量。比如我有5个分类,向量形如[0,0,1,0,0]或者[0,1,0,0,0]
  14. objectness!=1的AB或者说方块,不需要计算[tx, ty, th, tw]和classification

最终,我们得到了训练集上的objectness标签,location标签,classification标签。

为啥是sigmoid函数和自然对数e? 因为:以上四个算式都符合算法设计者对参数的约束条件。因此它是不唯一但是合理的代数表示。并且方程组是有解的。

BackBone骨干网络

所谓backbone就是我们做图像分类任务中的网络中截取的一部分。我们可以把它作为特征图提取来用。这部分网络统称骨干网络。AlexNet,VGG,RestNet以及它们的变种都有被用作BackBone。

Darknet53骨干网络

Darknet53

计算过程

以一张图片为例,原图表示为[1, 3, M, N],其中1表示一张图,3表示颜色通道,m = M/32 , n = N/32。其中32×32是我们划分AB时每个小方块的尺寸。

结合前面的分析,每个AB可以表示为位置4个维度,objectness1个维度,classification7个维度。总共12个维度表示一个AB。原图结合AB可以表示为数组:[1, 3×12, m, n] = [1, 36, m, n]

然而我们的Darknet53中C0的数组表示是:[1, 1024, m, n]。 因为它的输出通道为1024,stride为32。

为了能一步完成目标检测任务,我们需要通过一个叫YoloDetectionBlock的代码模块把1024个通道映射到AB区域上,也就是说要把1024个输出通道转换为36个输出通道使得算法能匹配每个AB所需要的特征图。同理C1,C2也是如此转换。

YoloDetectionBlock

我们仅仅以C0–>P0这个最基本的转换为例。下面的YoloDetectionBlock代码完成了从特征提取C0生成route(R0)和tip的工作。

注:route(R0)会用来跟C1做融合(concat)。

# 从骨干网络输出特征图C0得到跟预测相关的特征图P0
class YoloDetectionBlock(fluid.dygraph.Layer):
    # define YOLO-V3 detection head
    # 使用多层卷积和BN提取特征
    def __init__(self,ch_in,ch_out,is_test=True):
        super(YoloDetectionBlock, self).__init__()

        assert ch_out % 2 == 0, \
            "channel {} cannot be divided by 2".format(ch_out)

        self.conv0 = ConvBNLayer(
            ch_in=ch_in,
            ch_out=ch_out,
            filter_size=1,
            stride=1,
            padding=0,
            is_test=is_test
            )
        self.conv1 = ConvBNLayer(
            ch_in=ch_out,
            ch_out=ch_out*2,
            filter_size=3,
            stride=1,
            padding=1,
            is_test=is_test
            )
        self.conv2 = ConvBNLayer(
            ch_in=ch_out*2,
            ch_out=ch_out,
            filter_size=1,
            stride=1,
            padding=0,
            is_test=is_test
            )
        self.conv3 = ConvBNLayer(
            ch_in=ch_out,
            ch_out=ch_out*2,
            filter_size=3,
            stride=1,
            padding=1,
            is_test=is_test
            )
        self.route = ConvBNLayer(
            ch_in=ch_out*2,
            ch_out=ch_out,
            filter_size=1,
            stride=1,
            padding=0,
            is_test=is_test
            )
        self.tip = ConvBNLayer(
            ch_in=ch_out,
            ch_out=ch_out*2,
            filter_size=3,
            stride=1,
            padding=1,
            is_test=is_test
            )
    def forward(self, inputs):
        out = self.conv0(inputs)
        out = self.conv1(out)
        out = self.conv2(out)
        out = self.conv3(out)
        route = self.route(out)
        tip = self.tip(route)
        return route, tip

再经过一层卷积就得到了P0,实现T0–>P0:

# 添加从ti生成pi的模块,这是一个Conv2D操作,输出通道数为3 * (num_classes + 5)
block_out = self.add_sublayer(
    "block_out_%d" % (i),
    Conv2D(num_channels=512//(2**i)*2,
           num_filters=num_filters,
           filter_size=1,
           stride=1,
           padding=0,
           act=None,
           param_attr=ParamAttr(
               initializer=fluid.initializer.Normal(0., 0.02)),
           bias_attr=ParamAttr(
               initializer=fluid.initializer.Constant(0.0),
               regularizer=L2Decay(0.))))
self.block_outputs.append(block_out)

经过YOLOv3作者的精心设计经过网络转换后的数据含义如下:

P0[t,0:12,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第1个预测框所需要的12个预测值对应。

P0[t,12:24,i,j]P0[t, 12:24, i, j]P0[t,12:24,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第2个预测框所需要的12个预测值对应。

P0[t,24:36,i,j]P0[t, 24:36, i, j]P0[t,24:36,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第3个预测框所需要的12个预测值对应。

在P0中,P0[t,0:4,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第1个预测框的位置对应,P0[t,4,i,j]P0[t, 4, i, j]P0[t,4,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第1个预测框的objectness对应,P0[t,5:12,i,j]P0[t, 5:12, i, j]P0[t,5:12,i,j]与输入的第t张图片上小方块区域(i,j)(i, j)(i,j)第1个预测框的类别对应。

36个通道分成3组,每12个通道一组分别跟每个AB关联。

建立损失函数

我们有了训练集上的标签数据,又有了P0预测数据。那么我们就可以计算它们之间的损失函数了。

多尺度检测

因为我们有C0,C1,C2三个不同的尺度去计算特征图P0,P1,P2。其中C0最深,感受野最大,精度最高。但是它对小尺寸物体识别比较差。因此我们需要用C0–>P0的中间结果R0(因为R0数组做个简单的上采样大小就合适了)去和C1融合,既能够提供高精度,又能够在感受野比较小的区域识别小尺寸物体。同理C2。

Anchors依据的是COCO数据集中大小比较集中的9个尺寸,C0,C1,C2每个尺寸分别有3个AB。

下面是百度飞桨平台封装好的YOLOV3的损失函数调用方法:

    def get_loss(self, outputs, gtbox, gtlabel, gtscore=None,
                 anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
                 anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
                 ignore_thresh=0.7,
                 use_label_smooth=False):
        """
        使用fluid.layers.yolov3_loss,直接计算损失函数,过程更简洁,速度也更快
        """
        self.losses = []
        downsample = 32
        for i, out in enumerate(outputs): # 对三个层级分别求损失函数
            anchor_mask_i = anchor_masks[i]
            loss = fluid.layers.yolov3_loss(
                    x=out,  # out是P0, P1, P2中的一个
                    gt_box=gtbox,  # 真实框坐标
                    gt_label=gtlabel,  # 真实框类别
                    gt_score=gtscore,  # 真实框得分,使用mixup训练技巧时需要,不使用该技巧时直接设置为1,形状与gtlabel相同
                    anchors=anchors,   # 锚框尺寸,包含[w0, h0, w1, h1, ..., w8, h8]共9个锚框的尺寸
                    anchor_mask=anchor_mask_i, # 筛选锚框的mask,例如anchor_mask_i=[3, 4, 5],将anchors中第3、4、5个锚框挑选出来给该层级使用
                    class_num=self.num_classes, # 分类类别数
                    ignore_thresh=ignore_thresh, # 当预测框与真实框IoU > ignore_thresh,标注objectness = -1
                    downsample_ratio=downsample, # 特征图相对于原图缩小的倍数,例如P0是32, P1是16,P2是8
                    use_label_smooth=False)      # 使用label_smooth训练技巧时会用到,这里没用此技巧,直接设置为False
            self.losses.append(fluid.layers.reduce_mean(loss))  #reduce_mean对每张图片求和
            downsample = downsample // 2 # 下一级特征图的缩放倍数会减半
        return sum(self.losses) # 对每个层级求和

有了以上的基础代码,我们就可以开始训练AI识虫模型了。