Yolov4论文翻译与解析(二)

前言

上一篇主要对论文进行了翻译,这一篇结合一份代码详解下Yolov4相比较于v3采用的一些新技术和改进点,论文中其实已经对于一些技术梗概进行了分析,这里只对论文没有详述的部分进行一些自我的剖析,如果有不准确的部分,欢迎各位大神指教。本来打算用keras自己实现一遍YOLOv4,但有大神提前做了并进行了开源,就不重复造轮子了,本篇所有代码来源keras-yolo4
先放一张自己制作的Yolov4网络结构图:
在这里插入图片描述

主要技术点解析

CutMix和马赛克数据增强(Mosaic data augmentation)

Yolov4中除了采用常规的反转、裁切、旋转等方法外,额外主要采用了CutMix和马赛克数据增强(Mosaic data augmentation),如下图c、d所示,前者是将另一张图随机贴在一张图上,后者是将四张图拼接,目的都是使某些目标在脱离其常规的背景下进行训练,提高网络的鲁棒性,这个部分原论文有讲解以及实验比较结果,不多赘述。
在这里插入图片描述

Cross Stage Partial Network(CSP)

参考论文:CSPNET : A NEW BACKBONE THAT CAN ENHANCE LEARNING
CAPABILITY OF CNN

Yolov4在Backbone部分的一个主要改进点就是在ResBlock部分采用了CSP,相比较于原始的ResBlock,CSP将输入的特征图按照channel进行了切割,只使用原特征图的一半输入到残差网络中进行前向传播,另一半在最后与残差网络的输出结果直接进行按channel拼接(concatenate),这样做的好处在于:
1、输入只有一半参与了计算,可以大大减少计算量和内存消耗;
2、反向传播过程中,增加了一条完全独立的梯度传播路径,梯度信息不存在重复利用,如下图所示:
在这里插入图片描述
CSP论文中是用一个DenseNet举例的,差别不大,可以看到,(b)中 x 0 ′ x_{0'} x0相关的梯度与其他部分无关,直接向前传递,不通过DenseBlock,而(a)中所有梯度都通过DenseBlock,这个过程中许多梯度信息会被重复利用,具体可以参考论文中的推导。
代码实现:

def resblock_body(x, num_filters, num_blocks, all_narrow=True):
    '''A series of resblocks starting with a downsampling Convolution2D'''
    # Darknet uses left and top padding instead of 'same' mode
    '''1、首先进行下采样,将特征图大小减小到一半'''
    preconv1 = ZeroPadding2D(((1,0),(1,0)))(x)
    preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1)
    '''2、特征图切割,channel各为原特征图的一半'''
    shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
    mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)
    '''3、一半的特征图进入到残差网络中'''
    for i in range(num_blocks):
        y = compose(
                DarknetConv2D_BN_Mish(num_filters//2, (1,1)),
                DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (3,3)))(mainconv)
        mainconv = Add()([mainconv,y])
    postconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(mainconv)
    '''4、最后与另一半特征图进行拼接'''
    route = Concatenate()([postconv, shortconv])
    return DarknetConv2D_BN_Mish(num_filters, (1,1))(route)```

Spatial pyramid pooling(SPP)

这里使用的SPP module并不是之前大家常见的用于Faster RCNN中将特征图pooling到固定大小方便输入到全链接层的那个,而是一个用于CNN中扩大感受野的module,实现方法很简单,对一个特征图使用不同size的MaxPooling,并使用padding使输出大小与原图一致,最后对多个MaxPooling的结果进行堆叠,从而保证输出保留不同感受野的信息。
代码实现:

    y19 = DarknetConv2D_BN_Leaky(512, (1,1))(darknet.output)
    y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)
    y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)
    '''13*13 MaxPooling,输出19*19*512'''
    maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(y19)
    '''9*9 MaxPooling,输出19*19*512'''
    maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(y19)
    '''5*5 MaxPooling,输出19*19*512'''
    maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(y19)
    '''concatenate堆叠,输出19*19*2048'''
    y19 = Concatenate()([maxpool1, maxpool2, maxpool3, y19])
    y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)
    y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)
    y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)```

PANet

参考论文:Path Aggregation Network for Instance Segmentation
PAN是对于FPN的一种改进,对Yolov3熟悉的都知道,Yolov3的Neck部分实际上就是采用了FPN,只是细节上有一些调整,而PAN在FPN的基础上增加了一条从底到顶的Path,如图:
在这里插入图片描述
相比较于原始的FPN(a),其改进在于,原始的FPN的P5输出层,其包含的浅层特征丢失非常严重,如图中红线所示,浅层特征经过Backbone网络后,几乎都已经转化为深层特征,导致这个层的输出中能够利用的浅层特征很少了,在语义分割这样的任务中会导致边缘定位不准等问题。而在PAN中,浅层特征只需要经过绿色线路到达N5,这个线路的层数很少,可以保证浅层特征得到比较好的保存。Yolov4中做了进一步改进,在从底到顶的过程中,使用concatenate而不是addition来加入P5,如论文中图6所示。
代码太长这里就不贴了,位于yolo4_body函数中,分析起来也比较简单。

CmBN

如论文中图4,CmBn改进自CBN,参考论文Cross-Iteration Batch Normalization
BN层大家都比较熟悉了,用于解决前向传播过程中协防茶偏移问题,各种实验都证明了其有效性,但存在一个问题是,如果不能使用较大的batch size,则BN的效果会大大下降,如下图所示:
在这里插入图片描述
原理很简单,BN是假设每个batch统计出来的期望和方差能够反映整个训练集的情况,显然batch szie越小,误差越大,而在训练某些大型网络时或者只有性能相对一般的显卡时,设置过大的batch szie会导致内存爆掉等问题,CBM就是为了解决小batch szie的问题,它通过采集多个batch中的统计数据,进行合并作为本次iteration的统计数据进行计算,例如batch size为4,使用4个batch的数据相加,就等价于从16个样本中统计数据进行计算,但仍然存在一个问题,每一次iteration都会进行参数更新,也就是说以上4个batch前向传播使用的网络参数都不同,统计自然不能直接相加,论文中采用一种补偿的方法,基于相邻两个batch的计算时,参数变化基本是平滑的这个事实,采用泰勒多项式对前次统计数据进行补偿,如下式:
在这里插入图片描述
使用补偿后的数据进行计算即可,具体内容见CBN论文。
从Yolov4给出的BN、CBN和CmBN来看,其区别在于:

  • BN:无论每个batch被分割为多少个mini batch,其算法就是在每个mini batch前向传播后统计当前的BN数据(即每个神经元的期望和方差)并进行Nomalization,BN数据与其他mini batch的数据无关。
  • CBN:每次iteration中的BN数据是其之前n次数据和当前数据的和(对非当前batch统计的数据进行了补偿再参与计算),用该累加值对当前的batch进行Nomalization。好处在于每个batch可以设置较小的size。
  • CmBN:只在每个Batch内部使用CBN的方法,个人理解如果每个Batch被分割为一个mini batch,则其效果与BN一致;若分割为多个mini batch,则与CBN类似,只是把mini batch当作batch进行计算,其区别在于权重更新时间点不同,同一个batch内权重参数一样,因此计算不需要进行补偿。

Mish

引入了Mish激活函数,其公式如下:
M i s h = x ∗ t a n h ( l n ( 1 + e x ) ) Mish = x*tanh(ln(1+e^x)) Mish=xtanh(ln(1+ex))
在这里插入图片描述
对于激活函数没有过多研究,从近些年的观点来看,都希望其梯度变化尽可能平滑,Mish论文中的还提到一点,对于负值的轻微允许可以带来更好的梯度流。论文更多是用实验的方式证明其有效性。

Spatial Attention Module(SAM)

参考论文:CBAM: Convolutional Block Attention Module

Yolov4中只采用了CBAM中的一半,即SAM,而把Channel Attention Module去掉了,原因未知,原文中的SAM模块示意图如下:
在这里插入图片描述
对输入的特征图进行最大池化和平均池化,这里的池化操作是沿channel进行的,因此生成满足 M ∈ R h ∗ w ∗ 1 M\in R^{h*w*1} MRhw1的两张特征图,然后进过一个7*7的conv层,生成满足 M o u t ∈ R h ∗ w ∗ 1 M_{out}\in R^{h*w*1} MoutRhw1的SAM输出,再将这个输出与输入特征图相乘得到最终输出。
Yolov4对SAM进行了修改,没有使用池化操作,而是直接用卷积生成一个和输入特征图大小一样的特征图,经过sigmoid后再与原特征图按point-wise相乘,如原论文中中图5所示。
本文参考的这份代码中似乎没有SAM的实现,有空再细读下darknet原版实现。个人感觉这个修改还是蛮大的,作者没有给出详细的理论解释,实验部分也没有给出详细的与原版SAM的对比,稍显奇怪。

CIOU loss

在Yolov3中,关于box使用的是比较简单的MSE函数,这样存在两个弊病,1、割裂了每个box的shape或者point之间的相关性,一般box用两个point或一个point加wh这样表示,如果使用MSE,两个point或者point与w、h均单独计算loss,而实际上它们彼此间是存在相关性的,这样的计算相对不准确;2、loss值与shape相关,例如shape较大的box与较小的box,相对于真值偏移同样的比例,shape较大的box计算出的loss会大于shape较小的box,这是不合理的。IOU loss可以比较好的解决这两个问题。关于CIOU论文中也有说明,目前参考代码中只实现了GIOU和DIOU,这里先来看一下DIOU,有空再补全CIOU做下对比:

def box_diou(b1, b2):
    """
    Calculate DIoU loss on anchor boxes
    Reference Paper:
        "Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression"
        https://arxiv.org/abs/1911.08287

    Parameters
    ----------
    b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh
    b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh

    Returns
    -------
    diou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1)
    """
    # 把xy和wh表示的box转为[xmin,ymin]和[xmax,ymax]表示方便计算iou
    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

	# 计算重叠部分的[xmin,ymin]和[xmax,ymax]表示
    intersect_mins = K.maximum(b1_mins, b2_mins)
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)
    intersect_wh = K.maximum(intersect_maxes - intersect_mins, 0.)
    # 重叠部分面积
    intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
    b1_area = b1_wh[..., 0] * b1_wh[..., 1]
    b2_area = b2_wh[..., 0] * b2_wh[..., 1]
    # 计算IOU分母
    union_area = b1_area + b2_area - intersect_area
    # 计算IoU,防止被0除
    iou = intersect_area / (union_area + K.epsilon())

    # box中心点距离
    center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1)
    # 获取能把两个box都框住的外围框
    enclose_mins = K.minimum(b1_mins, b2_mins)
    enclose_maxes = K.maximum(b1_maxes, b2_maxes)
    enclose_wh = K.maximum(enclose_maxes - enclose_mins, 0.0)
    # 计算该外围框对角线长度
    enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1)
    # 计算DIOU,中心距离loss需要考虑两个box外围框的大小,是一种归一化手段
    diou = iou - 1.0 * (center_distance) / (enclose_diagonal + K.epsilon())

    # calculate param v and alpha to extend to CIoU
    #v = 4*K.square(tf.math.atan2(b1_wh[..., 0], b1_wh[..., 1]) - tf.math.atan2(b2_wh[..., 0], b2_wh[..., 1])) / (math.pi * math.pi)
    #alpha = v / (1.0 - iou + v)
    #diou = diou - alpha*v

    diou = K.expand_dims(diou, -1)
    return diou

总结

关于Yolov4的技术总结告一段落,有一些技术点例如对抗学习暂时还没有看到,后续会继续补全。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值