各位同学好,今天和大家分享一下使用Tensorflow2.0进行yolov3目标检测,如何构建Darknet53整体网络结构,如何使用特征金字塔强化特征提取。
yolov3借鉴了resnet的残差单元,在加深网络层数提高精度的同时大大降低了计算量。在yolov3中没有池化层和全连接层。张量的尺度变换是通过改变卷积核的步长来实现的(也就是通过卷积实现下采样)。例如stride=(2,2),相当于将图像边长缩小一般,整个特征图缩小2^2倍。详细见:目标检测之YOLO v3(附代码详细解析) - 知乎,下图为yolov3的网络结构。
卷积模块一般是指卷积层+批归一化+激活函数,在卷积层函数中,为了方便处理参数,设置*args用于接收卷积核个数和卷积核大小,**kwargs存放用于设置的权重正则化方法,在这里使用 keras.regularizers.l2正则化方法;当卷积层后面接批标准化BN层,就不需要使用偏置,会导致占用内存。
# 卷积
def Conv(x, *args, **kwargs):
# 列表一个*号: *args代表卷积核数量filter和尺寸kernel_size,
# 字典两个**号: **kwargs代表卷积部分其他参数,
new_kwargs = {'kernel_regularizer': l2(5e-4), 'use_bias':False} #l2正则化,有BN层不需要偏置
# new_kwargs是原来的字典参数,传进来新的参数,比如步长,需要更新字典参数
# 如果步长为2,填充方式需要变成'valid',步长为1填充方式为'same'
new_kwargs['padding'] = 'valid' if kwargs.get('strides') == (2,2) else 'same' #字典添加元素
# .get()返回指定key的值
# .update()把字典kwargs的键和值更新到new_kwargs中去
new_kwargs.update(kwargs) # 把步长更新进去,确定填充方式
# 卷积,使用更新后的参数
x = layers.Conv2D(*args, **new_kwargs)(x)
return x
# 卷积+BN+激活
def CBL(x, *args, **kwargs):
# 卷积
x = Conv(x, *args, **kwargs)
# 批归一化
x = layers.BatchNormalization()(x)
# 激活函数
x = LeakyReLU(alpha=0.1)(x)
return x
残差模块最显著的特点是使用了 short cut 机制(有点类似于电路中的短路机制)来缓解在神经网络中增加深度带来的梯度消失问题,从而使得神经网络变得更容易优化。它通过恒等映射(identity mapping)的方法使得输入和输出之间建立了一条直接的关联通道,从而使得网络集中学习输入和输出之间的残差。
使用layers.ZeroPadding2D()方法用0填充图像,size为偶数的图像在使用步长为(2,2)的卷积可以得到偶数size的图像。例如,[416,416,3]的图像0填充后变成[417,417,3],经过卷积核size=(3,3),步长strides=(2,2)的卷积,图像size变化为(417-3+1)/2 = 208
# 0填充+卷积+BN+激活
def PCBL(x, num_filter):
# 补0,将图片size补成奇数再降维得到偶数size
x = layers.ZeroPadding2D(((1,0),(1,0)))(x)
# 卷积层,下采样,步长为2
# 传入的*args有卷积核个数和大小,*kwargs有步长
x = CBL(x, num_filter, (3,3), strides=(2,2))
return x
# 2次(卷积+BN+激活) + 残差结构,需要把最后的输出与输入相加
def CBLR(x, num_filter):
# 卷积层
y = CBL(x, num_filter, (1,1)) # 步长默认为1
# 卷积层
y = CBL(y, num_filter*2, (3,3))
# 残差,add()相应元素相加,h,w,c都不变
x = layers.Add()([x,y]) #残差块,将输入和输出相加
return x
CBL5用于处理各尺度输出后的特征图,如最后一层大尺度目标检测的输出结果为[13,13,1024],经过五次卷积层进一步提取特征,从[13,13,1024]==>[13,13,512]==>[13,13,1024]==>[13,13,512]==>[13,13,1024]==>[13,13,512]
特征提取后的结果,一是用于大尺度目标检测;二是用于上采样函数CBLU,从[13,13,512]==>[26,26,256],长宽都变为原来的2倍,和中尺度输出的特征图像[26,26,512]叠加,即特征融合layers.Concatenate(),图像size不变,只增加通道数,得到叠加后的特征图的shape为[26,26,768],也是分别用于中尺度目标检测,和上采样后与小尺度图像叠加。
# 处理输出结果的各尺度图像
def CBL5(x, num_filter):
# 卷积+BN+激活
x = CBL(x, num_filter, (1,1))
x = CBL(x, num_filter*2, (3,3))
x = CBL(x, num_filter, (1,1))
x = CBL(x, num_filter*2, (3,3))
x = CBL(x, num_filter, (1,1))
return x
# 规整通道数得输出
def CBLC(x, num_filter, out_filter):
# 上升通道数,提特征
x = CBL(x, num_filter*2, (3,3))
# 卷积降维,规整通道数,得输出结果的通道数,根据需求设置
x = Conv(x, out_filter, (1,1))
return x
# 上采样用于下一个尺度--图像的size变大
def CBLU(x, num_filter):
# 1*1卷积传递图像信息
x = CBL(x, num_filter, (1,1))
# 上采样,宽高size各变成原来的2倍
x = layers.UpSampling2D(2)(x)
return x
darknet53网络结构如下:
使用特征金字塔FPN的思想,采用多尺度来对不同size的目标进行检测,这也对应了网络结构图的三个预测分支(输出)y1,y2,y3(每一个特征图会分配3个先验框,一共9个先验框)。y1,y2和y3的深度都是255,边长的规律是13:26:52,越小的特征图代表下采样的倍数高,而感受野也大,所以13×13的y1特征图对应检测大物体,26×26的y2特征图对应检测中物体,52×52的y3特征图对应检测小物体。
def body(inputs, num_anchors, num_classes): # 预选框数目、类别数目
# 存放最后三尺度的输出结果
out = []
# CBL层,卷积+BN+激活,给定卷积核32个,size(3,3)
x = CBL(inputs, 32, (3,3))
# CBLR在两两循环下的自身的循环次数
n = [1, 2, 8, 8, 4]
#(1)网络构建
# PCBL+CBLR出现连着出现五次
for i in range(5):
# PCBL==> zeropadding + conv + BN + 激活
x = PCBL(x, 2**(6+i)) #卷积核个数为2^(6+i)
# CBLR自身也有一个循环
for _ in range(n[i]):
# 残差结构
x = CBLR(x, 2**(5+i))
# darknet53构建完成
#(2)输出最后三尺度的图像
if i in [2,3,4]:
out.append(x)
#(3)处理每个尺度的图像
# ==1== 大尺度13*13*1024
x1 = CBL5(out[2], 512) #[13,13,512]
# 普通卷积得输出结果,将通道数变成输出结果的分类个数
y1 = CBLC(x1, 512, num_anchors*(num_classes + 5)) # 输出结果,和预选框的数目和识别物体的种类有关
# 5代表box的坐标(x,y), 宽w, 高h, 置信度c
# 上采样用于中尺度目标检测
x = CBLU(x1, 256) #[26,26,256]
# 大尺度上采样的返回结果与中尺度图像拼接
# Concatenate()拼接,h,w不变,通道数增加
x = layers.Concatenate()([x, out[1]]) #out[1]代表中尺度的图像
# [26,26,256] + [26,26,512] = [26,26,768]
# ==2== 中尺度26*26*768
x2 = CBL5(x, 256) #[26,26,256]
# 卷积得输出结果
y2 = CBLC(x2, 256, num_anchors*(num_classes + 5)) #[26,26,N]
# 中尺度上采样
x = CBLU(x2, 128) #[52,52,128]
# 与小尺度拼接
x = layers.Concatenate()([x, out[0]])
# [52,52,128] + [52,52,256] = [52,52,384]
# ==3== 小尺度52*52*384
x3 = CBL5(x, 128) # [52,52,128]
y3 = CBLC(x3, 128, num_anchors*(num_classes + 5)) #[52,52,N]
# 返回三个尺度信息
return [y1, y2, y3]
num_anchors = 3 # 预选框数目
num_classes = 5 # 识别种类,可改
# 输入层
input_tensor = keras.Input(shape=(416,416,3))
# 输出层,预选框数目和,分类数目
output_tensor = body(input_tensor, num_anchors, num_classes)
# 构建网络
model = Model(input_tensor, output_tensor)
# input_tensor是416*416*3的图片,output_tensor是大中小三个尺度的数组
# 查看网络结构
model.summary()
---------------------------------------------------------------
省略 N 层
---------------------------------------------------------------
Normalization)
batch_normalization_143 (Batch (None, 52, 52, 256) 1024 ['conv2d_148[0][0]']
Normalization)
leaky_re_lu_129 (LeakyReLU) (None, 13, 13, 1024 0 ['batch_normalization_129[0][0]']
)
leaky_re_lu_136 (LeakyReLU) (None, 26, 26, 512) 0 ['batch_normalization_136[0][0]']
leaky_re_lu_143 (LeakyReLU) (None, 52, 52, 256) 0 ['batch_normalization_143[0][0]']
conv2d_133 (Conv2D) (None, 13, 13, 30) 30720 ['leaky_re_lu_129[0][0]']
conv2d_141 (Conv2D) (None, 26, 26, 30) 15360 ['leaky_re_lu_136[0][0]']
conv2d_149 (Conv2D) (None, 52, 52, 30) 7680 ['leaky_re_lu_143[0][0]']
==================================================================================================
Total params: 61,597,792
Trainable params: 61,545,184
Non-trainable params: 52,608
__________________________________________________________________________________________________