前言
上一篇主要对论文进行了翻译,这一篇结合一份代码详解下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=x∗tanh(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}
M∈Rh∗w∗1的两张特征图,然后进过一个7*7的conv层,生成满足
M
o
u
t
∈
R
h
∗
w
∗
1
M_{out}\in R^{h*w*1}
Mout∈Rh∗w∗1的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的技术总结告一段落,有一些技术点例如对抗学习暂时还没有看到,后续会继续补全。