yolo系列目标检测算法的核心思想就是:把一张图片分为n*n网格,每个网格负责以该网格中心的区域的检测。
Yolo3使用的特征提取网络使用的是darknet53,它相较于其他yolo系列的检测算法主要改进有:
1、主干网络修改为darknet53,其重要特点是使用了残差网络Residual,darknet53中的残差卷积就是进行一次3X3、步长为2的卷积,然后保存该卷积layer,再进行一次1X1的卷积和一次3X3的卷积,并把这个结果加上layer作为最后的结果。
2、darknet53的每一个卷积部分使用了特有的DarknetConv2D结构,每一次卷积的时候进行L2正则化,完成卷积后进行BatchNormalization标准化与LeakyReLU。
这里以输入图片尺寸416x416x3、coco数据集分类数80为例来详细看一看Yolo3的整体网络结构图:
1.一个图片经过darknet53特征提取网络会依次得到6个特征层,
其大小分别是:
(416,416,32) (208,208,64) (104,104,128)
(52,52,256)(26,26,512)(13,13,1024),
2.后三个特征层再进行5次卷积处理,处理完后一部分用于输出该特征层对应的预测结果,一部分用于进行反卷积UmSampling2d后与其它特征层进行结合。(至于为什么要5次卷积我也不清楚)
3.经过一系列的卷积得到三个预测结果,其大小分别为:(52,52,255),(26,26,255),(13,13,255)
为什么是255呢? 这里是个需要好好理解的地方。
255 = 3*( 80 + 1 + 4 )
3表示的是:每一个网格所拥有的先验框的个数为3个(yolo3中默认设置的)
80代表的是:cooc数据集里面一共有80给类
1代表的是:每一个先验框所包含某个物体的置信度
4代表的是:预测框相对于每个网格中心点x轴偏移情况x_offset,预测框相对于每个网格中心点y轴偏移情况 y_offset, 先验框的height, 先验框的width。(每个网格点的中心加上x_offset和y_offset就可以得到预测框的中心,通过调整先验框的w和h就可以得到预测框的w和h,这样就可以确定预测框的位置啦)
PS:如果是voc数据集的话,最后的预测结果应该就是:(52,52,25)
(26,26,26)(13,13,25)。因为是20+1+4
以上就是yolo3特征提取+处理网络的输入和输出全部内容。
代码实现:
from functools import wraps
from keras.regularizers import l2
from keras.layers import Conv2D, BatchNormalization, Activation, ZeroPadding2D, Add, Input, UpSampling2D, Concatenate
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Model
from functools import reduce
#重新定义darknet中的Conv2D卷积操作
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
darknet_conv_kwargs = {'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)
#darknet卷积块: DarknetConv2D + BatchNormalization + Activation (三步)
def DarknetConv2D_BN_Leaky(*args, **kwargs):
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
return compose(DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(),
LeakyReLU(alpha=0.1))
#残差块 zeropadding + darknet卷积块 + shoutcut
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
#DarkNet53特征提取网络
def darknet53_body(input_tensor):
#shape 416, 416, 3 --> 416, 416, 32
x = DarknetConv2D_BN_Leaky(32, (3, 3))(input_tensor)
#shape 416, 416, 32 --> 208, 208, 64
x = Resblock_body(x, 64, 1)
#shape 208, 208, 64 --> 104, 104, 128
x = Resblock_body(x, 128, 2)
#shape 104, 104, 128 --> 52, 52, 256
x = Resblock_body(x, 256, 8)
feature_map1 = x
#shape 52, 52, 256 --> 26, 26, 512
x = Resblock_body(x, 512, 8)
feature_map2 = x
#shape 26, 26, 512 --> 13, 13, 1024
x = Resblock_body(x, 1024, 4)
feature_map3 = x
return feature_map1, feature_map2, feature_map3
#特征处理网络
def feature_to_output(x, num_filters, out_filters):
#五次卷积
x = DarknetConv2D_BN_Leaky(num_filters, (1, 1))(x)
x = DarknetConv2D_BN_Leaky(num_filters*2, (3, 3))(x)
x = DarknetConv2D_BN_Leaky(num_filters, (1, 1))(x)
x = DarknetConv2D_BN_Leaky(num_filters*2, (3, 3))(x)
x = DarknetConv2D_BN_Leaky(num_filters, (1, 1))(x)
#两个去处:1个进行3*3核1*1卷积直接输出 2.上采样跟上一级的特征层进行堆叠
y = DarknetConv2D_BN_Leaky(num_filters*2, (3, 3))(x)
y = DarknetConv2D(out_filters, (1, 1))(y)
return x, y
#yolo3的整体网络结构
def yolo3_body(inputs, num_anchors, num_classes):
#这里主要是对特征提取网路得到的三个特征层 进行处理
#52 52 256 26 26 512 13 13 1024
feature1, feature2, feature3 = darknet53_body(inputs)
darknet = Model(inputs, feature3)
#预测结果1 13 13 255 这里的255上文有解释
x, y1 = feature_to_output(darknet.output, 512, num_anchors*(num_classes+1+4))
x = DarknetConv2D_BN_Leaky(256, (1,1))(x) #卷积+上采样+feature2堆叠
x = UpSampling2D(2)(x)
x = Concatenate()([x, feature2])
#预测结果2 26 26 255
x, y2 = feature_to_output(x, 256, num_anchors * (num_classes + 1 + 4))
x = DarknetConv2D_BN_Leaky(128, (1, 1))(x)# 卷积+上采样+feature1堆叠
x = UpSampling2D(2)(x)
x = Concatenate()([x, feature1])
#预测结果3 52 52 255
x, y3 = feature_to_output(x, 128, num_anchors * (num_classes + 1 + 4))
return Model(inputs, [y1, y2, y3])
def compose(*funcs):
if funcs:
return reduce(lambda f, g: lambda *a, **kw: g(f(*a, **kw)), funcs)
else:
raise ValueError('Composition of empty sequence not supported.')
if __name__=="__main__":
inputs = Input(shape=(416, 416, 3))
yolo_model = yolo3_body(inputs, 3, 80)
yolo_model.summary()#打印模型
想更深入地了解之后的编码解码过程可以去看这个博客,那里讲的非常详细https://blog.csdn.net/weixin_44791964/article/details/103276106