睿智的目标检测29 您所在的位置:网站首页 yolov4激活函数 睿智的目标检测29

睿智的目标检测29

#睿智的目标检测29| 来源: 网络整理| 查看: 265

睿智的目标检测29——Keras搭建YoloV4目标检测平台 学习前言什么是YOLOV4代码下载YOLOV4改进的部分(不完全)YOLOV4结构解析1、主干特征提取网络Backbone2、特征金字塔3、YoloHead利用获得到的特征进行预测4、预测结果的解码5、在原图上进行绘制 YOLOV4的训练1、YOLOV4的改进训练技巧a)、Mosaic数据增强b)、Label Smoothing平滑c)、CIOUd)、学习率余弦退火衰减 2、loss组成a)、计算loss所需参数b)、y_pre是什么c)、y_true是什么。d)、loss的计算过程 训练自己的YoloV4模型一、数据集的准备二、数据集的处理三、开始网络训练四、训练结果预测

学习前言

哈哈哈我最喜欢的YOLO更新了! 在这里插入图片描述

什么是YOLOV4

在这里插入图片描述 YOLOV4是YOLOV3的改进版,在YOLOV3的基础上结合了非常多的小Tricks。 尽管没有目标检测上革命性的改变,但是YOLOV4依然很好的结合了速度与精度。 根据上图也可以看出来,YOLOV4在YOLOV3的基础上,在FPS不下降的情况下,mAP达到了44,提高非常明显。

YOLOV4整体上的检测思路和YOLOV3相比相差并不大,都是使用三个特征层进行分类与回归预测。

请注意!

强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

强烈建议在学习YOLOV4之前学习YOLOV3,因为YOLOV4确实可以看作是YOLOV3结合一系列改进的版本!

(重要的事情说三遍!)

YOLOV3可参考该博客: https://blog.csdn.net/weixin_44791964/article/details/103276106

代码下载

https://github.com/bubbliiiing/yolov4-keras 喜欢的可以给个star噢!

哔哩哔哩视频地址: https://www.bilibili.com/video/BV1kt4y117G6

YOLOV4改进的部分(不完全)

1、主干特征提取网络:DarkNet53 => CSPDarkNet53

2、特征金字塔:SPP,PAN

3、分类回归层:YOLOv3(未改变)

4、训练用到的小技巧:Mosaic数据增强、Label Smoothing平滑、CIOU、学习率余弦退火衰减

5、激活函数:使用Mish激活函数

以上并非全部的改进部分,还存在一些其它的改进,由于YOLOV4使用的改进实在太多了,很难完全实现与列出来,这里只列出来了一些我比较感兴趣,而且非常有效的改进。

还有一个重要的事情: 论文中提到的SAM,作者自己的源码也没有使用。

还有其它很多的tricks,不是所有的tricks都有提升,我也没法实现全部的tricks。

整篇BLOG会结合YOLOV3与YOLOV4的差别进行解析

YOLOV4结构解析 1、主干特征提取网络Backbone

当输入是416x416时,特征结构如下: 在这里插入图片描述 当输入是608x608时,特征结构如下: 在这里插入图片描述 主干特征提取网络Backbone的改进点有两个: a).主干特征提取网络:DarkNet53 => CSPDarkNet53 b).激活函数:使用Mish激活函数

如果大家对YOLOV3比较熟悉的话,应该知道Darknet53的结构,其由一系列残差网络结构构成。在Darknet53中,其存在如下resblock_body模块,其由一次下采样和多次残差结构的堆叠构成,Darknet53便是由resblock_body模块组合而成。

def resblock_body(x, num_filters, num_blocks): x = ZeroPadding2D(((1,0),(1,0)))(x) x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x) for i in range(num_blocks): y = DarknetConv2D_BN_Leaky(num_filters//2, (1,1))(x) y = DarknetConv2D_BN_Leaky(num_filters, (3,3))(y) x = Add()([x,y]) return x

而在YOLOV4中,其对该部分进行了一定的修改。 1、其一是将DarknetConv2D的激活函数由LeakyReLU修改成了Mish,卷积块由DarknetConv2D_BN_Leaky变成了DarknetConv2D_BN_Mish。 Mish函数的公式与图像如下: M i s h = x × t a n h ( l n ( 1 + e x ) ) Mish=x \times tanh(ln(1+e^x)) Mish=x×tanh(ln(1+ex)) 在这里插入图片描述 2、其二是将resblock_body的结构进行修改,使用了CSPnet结构。此时YOLOV4当中的Darknet53被修改成了CSPDarknet53。 在这里插入图片描述 CSPnet结构并不算复杂,就是将原来的残差块的堆叠进行了一个拆分,拆成左右两部分: 主干部分继续进行原来的残差块的堆叠; 另一部分则像一个残差边一样,经过少量处理直接连接到最后。 因此可以认为CSP中存在一个大的残差边。

#--------------------------------------------------------------------# # CSPdarknet的结构块 # 首先利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩 # 然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构 # 主干部分会对num_blocks进行循环,循环内部是残差结构。 # 对于整个CSPdarknet的结构块,就是一个大残差块+内部多个小残差块 #--------------------------------------------------------------------# def resblock_body(x, num_filters, num_blocks, all_narrow=True): #----------------------------------------------------------------# # 利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩 #----------------------------------------------------------------# preconv1 = ZeroPadding2D(((1,0),(1,0)))(x) preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1) #--------------------------------------------------------------------# # 然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构 #--------------------------------------------------------------------# shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1) #----------------------------------------------------------------# # 主干部分会对num_blocks进行循环,循环内部是残差结构。 #----------------------------------------------------------------# mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1) 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) #----------------------------------------------------------------# # 将大残差边再堆叠回来 #----------------------------------------------------------------# route = Concatenate()([postconv, shortconv]) # 最后对通道数进行整合 return DarknetConv2D_BN_Mish(num_filters, (1,1))(route)

全部实现代码为:

from functools import wraps from keras import backend as K from keras.initializers import random_normal from keras.layers import (Add, BatchNormalization, Concatenate, Conv2D, Layer, ZeroPadding2D) from keras.layers.normalization import BatchNormalization from keras.regularizers import l2 from utils.utils import compose class Mish(Layer): def __init__(self, **kwargs): super(Mish, self).__init__(**kwargs) self.supports_masking = True def call(self, inputs): return inputs * K.tanh(K.softplus(inputs)) def get_config(self): config = super(Mish, self).get_config() return config def compute_output_shape(self, input_shape): return input_shape #------------------------------------------------------# # 单次卷积DarknetConv2D # 如果步长为2则自己设定padding方式。 #------------------------------------------------------# @wraps(Conv2D) def DarknetConv2D(*args, **kwargs): darknet_conv_kwargs = {'kernel_initializer' : random_normal(stddev=0.02), 'kernel_regularizer': l2(5e-4)} darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same' darknet_conv_kwargs.update(kwargs) return Conv2D(*args, **darknet_conv_kwargs) #---------------------------------------------------# # 卷积块 -> 卷积 + 标准化 + 激活函数 # DarknetConv2D + BatchNormalization + Mish #---------------------------------------------------# def DarknetConv2D_BN_Mish(*args, **kwargs): no_bias_kwargs = {'use_bias': False} no_bias_kwargs.update(kwargs) return compose( DarknetConv2D(*args, **no_bias_kwargs), BatchNormalization(), Mish()) #--------------------------------------------------------------------# # CSPdarknet的结构块 # 首先利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩 # 然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构 # 主干部分会对num_blocks进行循环,循环内部是残差结构。 # 对于整个CSPdarknet的结构块,就是一个大残差块+内部多个小残差块 #--------------------------------------------------------------------# def resblock_body(x, num_filters, num_blocks, all_narrow=True): #----------------------------------------------------------------# # 利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩 #----------------------------------------------------------------# preconv1 = ZeroPadding2D(((1,0),(1,0)))(x) preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1) #--------------------------------------------------------------------# # 然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构 #--------------------------------------------------------------------# shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1) #----------------------------------------------------------------# # 主干部分会对num_blocks进行循环,循环内部是残差结构。 #----------------------------------------------------------------# mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1) 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) #----------------------------------------------------------------# # 将大残差边再堆叠回来 #----------------------------------------------------------------# route = Concatenate()([postconv, shortconv]) # 最后对通道数进行整合 return DarknetConv2D_BN_Mish(num_filters, (1,1))(route) #---------------------------------------------------# # CSPdarknet53 的主体部分 # 输入为一张416x416x3的图片 # 输出为三个有效特征层 #---------------------------------------------------# def darknet_body(x): x = DarknetConv2D_BN_Mish(32, (3,3))(x) x = resblock_body(x, 64, 1, False) x = resblock_body(x, 128, 2) x = resblock_body(x, 256, 8) feat1 = x x = resblock_body(x, 512, 8) feat2 = x x = resblock_body(x, 1024, 4) feat3 = x return feat1,feat2,feat3 2、特征金字塔

当输入是416x416时,特征结构如下: 在这里插入图片描述 当输入是608x608时,特征结构如下: 在这里插入图片描述 在特征金字塔部分,YOLOV4结合了两种改进: a).使用了SPP结构。 b).使用了PANet结构。 如上图所示,除去CSPDarknet53和Yolo Head的结构外,都是特征金字塔的结构。 1、SPP结构参杂在对CSPdarknet53的最后一个特征层的卷积里,在对CSPdarknet53的最后一个特征层进行三次DarknetConv2D_BN_Leaky卷积后,分别利用四个不同尺度的最大池化进行处理,最大池化的池化核大小分别为13x13、9x9、5x5、1x1(1x1即无处理)

# 使用了SPP结构,即不同尺度的最大池化后堆叠。 maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(P5) maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(P5) maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(P5) P5 = Concatenate()([maxpool1, maxpool2, maxpool3, P5])

其可以它能够极大地增加感受野,分离出最显著的上下文特征。 在这里插入图片描述 2、PANet是2018的一种实例分割算法,其具体结构由反复提升特征的意思。 在这里插入图片描述 上图为原始的PANet的结构,可以看出来其具有一个非常重要的特点就是特征的反复提取。 在(a)里面是传统的特征金字塔结构,在完成特征金字塔从下到上的特征提取后,还需要实现(b)中从上到下的特征提取。

而在YOLOV4当中,其主要是在三个有效特征层上使用了PANet结构。 在这里插入图片描述 实现代码如下:

#---------------------------------------------------# # Panet网络的构建,并且获得预测结果 #---------------------------------------------------# def yolo_body(input_shape, anchors_mask, num_classes): inputs = Input(input_shape) #---------------------------------------------------# # 生成CSPdarknet53的主干模型 # 获得三个有效特征层,他们的shape分别是: # 52,52,256 # 26,26,512 # 13,13,1024 #---------------------------------------------------# feat1,feat2,feat3 = darknet_body(inputs) # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,2048 -> 13,13,512 -> 13,13,1024 -> 13,13,512 P5 = DarknetConv2D_BN_Leaky(512, (1,1))(feat3) P5 = DarknetConv2D_BN_Leaky(1024, (3,3))(P5) P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5) # 使用了SPP结构,即不同尺度的最大池化后堆叠。 maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(P5) maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(P5) maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(P5) P5 = Concatenate()([maxpool1, maxpool2, maxpool3, P5]) P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5) P5 = DarknetConv2D_BN_Leaky(1024, (3,3))(P5) P5 = DarknetConv2D_BN_Leaky(512, (1,1))(P5) # 13,13,512 -> 13,13,256 -> 26,26,256 P5_upsample = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(P5) # 26,26,512 -> 26,26,256 P4 = DarknetConv2D_BN_Leaky(256, (1,1))(feat2) # 26,26,256 + 26,26,256 -> 26,26,512 P4 = Concatenate()([P4, P5_upsample]) # 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 P4 = make_five_convs(P4,256) # 26,26,256 -> 26,26,128 -> 52,52,128 P4_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(P4) # 52,52,256 -> 52,52,128 P3 = DarknetConv2D_BN_Leaky(128, (1,1))(feat1) # 52,52,128 + 52,52,128 -> 52,52,256 P3 = Concatenate()([P3, P4_upsample]) # 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128 P3 = make_five_convs(P3,128) #---------------------------------------------------# # 第三个特征层 # y3=(batch_size,52,52,3,85) #---------------------------------------------------# P3_output = DarknetConv2D_BN_Leaky(256, (3,3))(P3) P3_output = DarknetConv2D(len(anchors_mask[0])*(num_classes+5), (1,1))(P3_output) # 52,52,128 -> 26,26,256 P3_downsample = ZeroPadding2D(((1,0),(1,0)))(P3) P3_downsample = DarknetConv2D_BN_Leaky(256, (3,3), strides=(2,2))(P3_downsample) # 26,26,256 + 26,26,256 -> 26,26,512 P4 = Concatenate()([P3_downsample, P4]) # 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 P4 = make_five_convs(P4,256) #---------------------------------------------------# # 第二个特征层 # y2=(batch_size,26,26,3,85) #---------------------------------------------------# P4_output = DarknetConv2D_BN_Leaky(512, (3,3))(P4) P4_output = DarknetConv2D(len(anchors_mask[1])*(num_classes+5), (1,1))(P4_output) # 26,26,256 -> 13,13,512 P4_downsample = ZeroPadding2D(((1,0),(1,0)))(P4) P4_downsample = DarknetConv2D_BN_Leaky(512, (3,3), strides=(2,2))(P4_downsample) # 13,13,512 + 13,13,512 -> 13,13,1024 P5 = Concatenate()([P4_downsample, P5]) # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 P5 = make_five_convs(P5,512) #---------------------------------------------------# # 第一个特征层 # y1=(batch_size,13,13,3,85) #---------------------------------------------------# P5_output = DarknetConv2D_BN_Leaky(1024, (3,3))(P5) P5_output = DarknetConv2D(len(anchors_mask[2])*(num_classes+5), (1,1))(P5_output) return Model(inputs, [P5_output, P4_output, P3_output]) 3、YoloHead利用获得到的特征进行预测

当输入是416x416时,特征结构如下: 在这里插入图片描述 当输入是608x608时,特征结构如下: 在这里插入图片描述 1、在特征利用部分,YoloV4提取多特征层进行目标检测,一共提取三个特征层,分别位于中间层,中下层,底层,三个特征层的shape分别为(52,52,256)、(26,26,512)、(13,13,1024)。

2、输出层的shape分别为(13,13,75),(26,26,75),(52,52,75),最后一个维度为75是因为该图是基于voc数据集的,它的类为20种,YoloV4只有针对每一个特征层存在3个先验框,所以最后维度为3x25; 如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255)

实现代码如下:

#---------------------------------------------------# # 特征层->最后的输出 #---------------------------------------------------# def yolo_body(inputs, num_anchors, num_classes): # 省略了一部分,只看最后的head部分 P3_output = DarknetConv2D_BN_Leaky(256, (3,3))(P3) P3_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P3_output) P4_output = DarknetConv2D_BN_Leaky(512, (3,3))(P4) P4_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P4_output) P5_output = DarknetConv2D_BN_Leaky(1024, (3,3))(P5) P5_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(P5_output) 4、预测结果的解码

由第二步我们可以获得三个特征层的预测结果,shape分别为(N,13,13,255),(N,26,26,255),(N,52,52,255)的数据,对应每个图分为13x13、26x26、52x52的网格上3个预测框的位置。

但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。

此处要讲一下yolo3的预测原理,yolo3的3个特征层分别将整幅图分为13x13、26x26、52x52的网格,每个网络点负责一个区域的检测。

我们知道特征层的预测结果对应着三个预测框的位置,我们先将其reshape一下,其结果为(N,13,13,3,85),(N,26,26,3,85),(N,52,52,3,85)。

最后一个维度中的85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果。

yolo3的解码过程就是将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心,然后再利用 先验框和h、w结合 计算出预测框的长和宽。这样就能得到整个预测框的位置了。

在这里插入图片描述 当然得到最终的预测结构后还要进行得分排序与非极大抑制筛选 这一部分基本上是所有目标检测通用的部分。不过该项目的处理方式与其它项目不同。其对于每一个类进行判别。 1、取出每一类得分大于self.obj_threshold的框和得分。 2、利用框的位置和得分进行非极大抑制。

实现代码如下,当调用yolo_eval时,就会对每个特征层进行解码:

import tensorflow as tf from keras import backend as K #---------------------------------------------------# # 对box进行调整,使其符合真实图片的样子 #---------------------------------------------------# def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image): #-----------------------------------------------------------------# # 把y轴放前面是因为方便预测框和图像的宽高进行相乘 #-----------------------------------------------------------------# box_yx = box_xy[..., ::-1] box_hw = box_wh[..., ::-1] input_shape = K.cast(input_shape, K.dtype(box_yx)) image_shape = K.cast(image_shape, K.dtype(box_yx)) if letterbox_image: #-----------------------------------------------------------------# # 这里求出来的offset是图像有效区域相对于图像左上角的偏移情况 # new_shape指的是宽高缩放情况 #-----------------------------------------------------------------# new_shape = K.round(image_shape * K.min(input_shape/image_shape)) offset = (input_shape - new_shape)/2./input_shape scale = input_shape/new_shape box_yx = (box_yx - offset) * scale box_hw *= scale box_mins = box_yx - (box_hw / 2.) box_maxes = box_yx + (box_hw / 2.) boxes = K.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]]) boxes *= K.concatenate([image_shape, image_shape]) return boxes #---------------------------------------------------# # 将预测值的每个特征层调成真实值 #---------------------------------------------------# def get_anchors_and_decode(feats, anchors, num_classes, input_shape, calc_loss=False): num_anchors = len(anchors) #------------------------------------------# # grid_shape指的是特征层的高和宽 #------------------------------------------# grid_shape = K.shape(feats)[1:3] #--------------------------------------------------------------------# # 获得各个特征点的坐标信息。生成的shape为(13, 13, num_anchors, 2) #--------------------------------------------------------------------# grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]), [grid_shape[0], 1, num_anchors, 1]) grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]), [1, grid_shape[1], num_anchors, 1]) grid = K.cast(K.concatenate([grid_x, grid_y]), K.dtype(feats)) #---------------------------------------------------------------# # 将先验框进行拓展,生成的shape为(13, 13, num_anchors, 2) #---------------------------------------------------------------# anchors_tensor = K.reshape(K.constant(anchors), [1, 1, num_anchors, 2]) anchors_tensor = K.tile(anchors_tensor, [grid_shape[0], grid_shape[1], 1, 1]) #---------------------------------------------------# # 将预测结果调整成(batch_size,13,13,3,85) # 85可拆分成4 + 1 + 80 # 4代表的是中心宽高的调整参数 # 1代表的是框的置信度 # 80代表的是种类的置信度 #---------------------------------------------------# feats = K.reshape(feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5]) #------------------------------------------# # 对先验框进行解码,并进行归一化 #------------------------------------------# box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats)) box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats)) #------------------------------------------# # 获得预测框的置信度 #------------------------------------------# box_confidence = K.sigmoid(feats[..., 4:5]) box_class_probs = K.sigmoid(feats[..., 5:]) #---------------------------------------------------------------------# # 在计算loss的时候返回grid, feats, box_xy, box_wh # 在预测的时候返回box_xy, box_wh, box_confidence, box_class_probs #---------------------------------------------------------------------# if calc_loss == True: return grid, feats, box_xy, box_wh return box_xy, box_wh, box_confidence, box_class_probs #---------------------------------------------------# # 图片预测 #---------------------------------------------------# def DecodeBox(outputs, anchors, num_classes, image_shape, input_shape, #-----------------------------------------------------------# # 13x13的特征层对应的anchor是[116,90],[156,198],[373,326] # 26x26的特征层对应的anchor是[30,61],[62,45],[59,119] # 52x52的特征层对应的anchor是[10,13],[16,30],[33,23] #-----------------------------------------------------------# anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]], max_boxes = 100, confidence = 0.5, nms_iou = 0.3, letterbox_image = True): box_xy = [] box_wh = [] box_confidence = [] box_class_probs = [] for i in range(len(outputs)): sub_box_xy, sub_box_wh, sub_box_confidence, sub_box_class_probs = \ get_anchors_and_decode(outputs[i], anchors[anchor_mask[i]], num_classes, input_shape) box_xy.append(K.reshape(sub_box_xy, [-1, 2])) box_wh.append(K.reshape(sub_box_wh, [-1, 2])) box_confidence.append(K.reshape(sub_box_confidence, [-1, 1])) box_class_probs.append(K.reshape(sub_box_class_probs, [-1, num_classes])) box_xy = K.concatenate(box_xy, axis = 0) box_wh = K.concatenate(box_wh, axis = 0) box_confidence = K.concatenate(box_confidence, axis = 0) box_class_probs = K.concatenate(box_class_probs, axis = 0) #------------------------------------------------------------------------------------------------------------# # 在图像传入网络预测前会进行letterbox_image给图像周围添加灰条,因此生成的box_xy, box_wh是相对于有灰条的图像的 # 我们需要对其进行修改,去除灰条的部分。 将box_xy、和box_wh调节成y_min,y_max,xmin,xmax # 如果没有使用letterbox_image也需要将归一化后的box_xy, box_wh调整成相对于原图大小的 #------------------------------------------------------------------------------------------------------------# boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image) box_scores = box_confidence * box_class_probs #-----------------------------------------------------------# # 判断得分是否大于score_threshold #-----------------------------------------------------------# mask = box_scores >= confidence max_boxes_tensor = K.constant(max_boxes, dtype='int32') boxes_out = [] scores_out = [] classes_out = [] for c in range(num_classes): #-----------------------------------------------------------# # 取出所有box_scores >= score_threshold的框,和成绩 #-----------------------------------------------------------# class_boxes = tf.boolean_mask(boxes, mask[:, c]) class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c]) #-----------------------------------------------------------# # 非极大抑制 # 保留一定区域内得分最大的框 #-----------------------------------------------------------# nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=nms_iou) #-----------------------------------------------------------# # 获取非极大抑制后的结果 # 下列三个分别是:框的位置,得分与种类 #-----------------------------------------------------------# class_boxes = K.gather(class_boxes, nms_index) class_box_scores = K.gather(class_box_scores, nms_index) classes = K.ones_like(class_box_scores, 'int32') * c boxes_out.append(class_boxes) scores_out.append(class_box_scores) classes_out.append(classes) boxes_out = K.concatenate(boxes_out, axis=0) scores_out = K.concatenate(scores_out, axis=0) classes_out = K.concatenate(classes_out, axis=0) return boxes_out, scores_out, classes_out 5、在原图上进行绘制

通过第四步,我们可以获得预测框在原图上的位置,而且这些预测框都是经过筛选的。这些筛选后的框可以直接绘制在图片上,就可以获得结果了。

YOLOV4的训练 1、YOLOV4的改进训练技巧 a)、Mosaic数据增强

Yolov4的mosaic数据增强参考了CutMix数据增强方式,理论上具有一定的相似性! CutMix数据增强方式利用两张图片进行拼接。 在这里插入图片描述 但是mosaic利用了四张图片,根据论文所说其拥有一个巨大的优点是丰富检测物体的背景!且在BN计算的时候一下子会计算四张图片的数据! 就像下图这样: 在这里插入图片描述 实现思路如下: 1、每次读取四张图片。

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 2、分别对四张图片进行翻转、缩放、色域变化等,并且按照四个方向位置摆好。 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 3、进行图片的组合和框的组合 在这里插入图片描述

def merge_bboxes(self, bboxes, cutx, cuty): merge_bbox = [] for i in range(len(bboxes)): for box in bboxes[i]: tmp_box = [] x1, y1, x2, y2 = box[0], box[1], box[2], box[3] if i == 0: if y1 > cuty or x1 > cutx: continue if y2 >= cuty and y1 = cutx and x1 = cuty and y1 = cutx and x1 1, box_h>1)] box_data = np.zeros((len(box),5)) box_data[:len(box)] = box image_datas.append(image_data) box_datas.append(box_data) # 将图片分割,放在一起 cutx = int(w * min_offset_x) cuty = int(h * min_offset_y) new_image = np.zeros([h, w, 3]) new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :] new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :] new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :] new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :] # 进行色域变换 hue = self.rand(-hue, hue) sat = self.rand(1, sat) if self.rand()1] = 1 x[x0: if len(new_boxes)>max_boxes: new_boxes = new_boxes[:max_boxes] box_data[:len(new_boxes)] = new_boxes return new_image, box_data b)、Label Smoothing平滑

标签平滑的思想很简单,具体公式如下:

new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes

当label_smoothing的值为0.01得时候,公式变成如下所示:

new_onehot_labels = y * (1 - 0.01) + 0.01 / num_classes

其实Label Smoothing平滑就是将标签进行一个平滑,原始的标签是0、1,在平滑后变成0.005(如果是二分类)、0.995,也就是说对分类准确做了一点惩罚,让模型不可以分类的太准确,太准确容易过拟合。

实现代码如下:

#---------------------------------------------------# # 平滑标签 #---------------------------------------------------# def _smooth_labels(y_true, label_smoothing): num_classes = K.shape(y_true)[-1], label_smoothing = K.constant(label_smoothing, dtype=K.floatx()) return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes c)、CIOU

IoU是比值的概念,对目标物体的scale是不敏感的。然而常用的BBox的回归损失优化和IoU优化不是完全等价的,寻常的IoU无法直接优化没有重叠的部分。

于是有人提出直接使用IOU作为回归优化loss,CIOU是其中非常优秀的一种想法。

CIOU将目标与anchor之间的距离,重叠率、尺度以及惩罚项都考虑进去,使得目标框回归变得更加稳定,不会像IoU和GIoU一样出现训练过程中发散等问题。而惩罚因子把预测框长宽比拟合目标框的长宽比考虑进去。

在这里插入图片描述 CIOU公式如下 C I O U = I O U − ρ 2 ( b , b g t ) c 2 − α v CIOU = IOU - \frac{\rho^2(b,b^{gt})}{c^2} - \alpha v CIOU=IOU−c2ρ2(b,bgt)​−αv 其中, ρ 2 ( b , b g t ) \rho^2(b,b^{gt}) ρ2(b,bgt)分别代表了预测框和真实框的中心点的欧式距离。 c代表的是能够同时包含预测框和真实框的最小闭包区域的对角线距离。

而 α \alpha α和 v v v的公式如下 α = v 1 − I O U + v \alpha = \frac{v}{1-IOU+v} α=1−IOU+vv​ v = 4 π 2 ( a r c t a n w g t h g t − a r c t a n w h ) 2 v = \frac{4}{\pi ^2}(arctan\frac{w^{gt}}{h^{gt}}-arctan\frac{w}{h})^2 v=π24​(arctanhgtwgt​−arctanhw​)2 把1-CIOU就可以得到相应的LOSS了。 L O S S C I O U = 1 − I O U + ρ 2 ( b , b g t ) c 2 + α v LOSS_{CIOU} = 1 - IOU + \frac{\rho^2(b,b^{gt})}{c^2} + \alpha v LOSSCIOU​=1−IOU+c2ρ2(b,bgt)​+αv

def box_ciou(b1, b2): """ 输入为: ---------- b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh 返回为: ------- ciou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1) """ #-----------------------------------------------------------# # 求出预测框左上角右下角 # b1_mins (batch, feat_w, feat_h, anchor_num, 2) # b1_maxes (batch, feat_w, feat_h, anchor_num, 2) #-----------------------------------------------------------# 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_mins (batch, feat_w, feat_h, anchor_num, 2) # b2_maxes (batch, feat_w, feat_h, anchor_num, 2) #-----------------------------------------------------------# 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 #-----------------------------------------------------------# # 求真实框和预测框所有的iou # iou (batch, feat_w, feat_h, anchor_num) #-----------------------------------------------------------# 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] union_area = b1_area + b2_area - intersect_area iou = intersect_area / K.maximum(union_area, K.epsilon()) #-----------------------------------------------------------# # 计算中心的差距 # center_distance (batch, feat_w, feat_h, anchor_num) #-----------------------------------------------------------# center_distance = K.sum(K.square(b1_xy - b2_xy), axis=-1) 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 (batch, feat_w, feat_h, anchor_num) #-----------------------------------------------------------# enclose_diagonal = K.sum(K.square(enclose_wh), axis=-1) ciou = iou - 1.0 * (center_distance) / K.maximum(enclose_diagonal ,K.epsilon()) v = 4 * K.square(tf.math.atan2(b1_wh[..., 0], K.maximum(b1_wh[..., 1], K.epsilon())) - tf.math.atan2(b2_wh[..., 0], K.maximum(b2_wh[..., 1],K.epsilon()))) / (math.pi * math.pi) alpha = v / K.maximum((1.0 - iou + v), K.epsilon()) ciou = ciou - alpha * v ciou = K.expand_dims(ciou, -1) return ciou d)、学习率余弦退火衰减

余弦退火衰减法,学习率会先上升再下降,这是退火优化法的思想。(关于什么是退火算法可以百度。)

上升的时候使用线性上升,下降的时候模拟cos函数下降。执行多次。

效果如图所示: 在这里插入图片描述

实现方式如下,利用Callback实现,与普通的ReduceLROnPlateau调用方式类似:

class WarmUpCosineDecayScheduler(keras.callbacks.Callback): def __init__(self, T_max, eta_min=0, verbose=0): super(WarmUpCosineDecayScheduler, self).__init__() self.T_max = T_max self.eta_min = eta_min self.verbose = verbose self.init_lr = 0 self.last_epoch = 0 def on_train_begin(self, batch, logs=None): self.init_lr = K.get_value(self.model.optimizer.lr) def on_epoch_end(self, batch, logs=None): learning_rate = self.eta_min + (self.init_lr - self.eta_min) * (1 + math.cos(math.pi * self.last_epoch / self.T_max)) / 2 self.last_epoch += 1 K.set_value(self.model.optimizer.lr, learning_rate) if self.verbose > 0: print('Setting learning rate to %s.' % (learning_rate)) 2、loss组成 a)、计算loss所需参数

在计算loss的时候,实际上是y_pre和y_true之间的对比: y_pre就是一幅图像经过网络之后的输出,内部含有三个特征层的内容;其需要解码才能够在图上作画 y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致 实际上y_pre和y_true内容的shape都是 (batch_size,13,13,3,85) (batch_size,26,26,3,85) (batch_size,52,52,3,85)

b)、y_pre是什么

网络最后输出的内容就是三个特征层每个网格点对应的预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后,每个网格点上三个先验框对应的位置、置信度及其种类。 对于输出的y1、y2、y3而言,[…, : 2]指的是相对于每个网格点的偏移量,[…, 2: 4]指的是宽和高,[…, 4: 5]指的是该框的置信度,[…, 5: ]指的是每个种类的预测概率。 现在的y_pre还是没有解码的,解码了之后才是真实图像上的情况。

c)、y_true是什么。

y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致 在yolo4中,其使用了一个专门的函数用于处理读取进来的图片的框的真实情况。

def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):

其输入为: true_boxes:shape为(m, T, 5)代表m张图T个框的x_min、y_min、x_max、y_max、class_id。 input_shape:输入的形状,此处为608、608 anchors:代表9个先验框的大小 num_classes:种类的数量。 其实对真实框的处理是将真实框转化成图片中相对网格的xyhw,步骤如下: 1、取框的真实值,获取其框的中心及其宽高,除去input_shape变成比例的模式。 2、建立全为0的y_true,y_true是一个列表,包含三个特征层,shape分别为(batch_size,13,13,3,85)、(batch_size,26,26,3,85)、(batch_size,52,52,3,85)。 3、对每一张图片处理,将每一张图片中的真实框的wh和先验框的wh对比,计算IOU值,选取其中IOU最高的一个,得到其所属特征层及其网格点的位置,在对应的y_true中将内容进行保存。

for t, n in enumerate(best_anchor): for l in range(num_layers): if n in anchor_mask[l]: # 计算该目标在第l个特征层所处网格的位置 i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32') j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32') # 找到best_anchor索引的索引 k = anchor_mask[l].index(n) c = true_boxes[b,t, 4].astype('int32') # 保存到y_true中 y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4] y_true[l][b, j, i, k, 4] = 1 y_true[l][b, j, i, k, 5+c] = 1

对于最后输出的y_true而言,只有每个图里每个框最对应的位置有数据,其它的地方都为0。 preprocess_true_boxes全部的代码如下:

def preprocess_true_boxes(self, true_boxes, input_shape, anchors, num_classes): assert (true_boxes[..., 4]


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有