zoukankan      html  css  js  c++  java
  • 探索 YOLO v3 源码

    C. L. Wang 深度算法 2018-08-06

    YOLO,即You Only Look Once的缩写,是一个基于卷积神经网络的物体检测算法。而YOLO v3是YOLO的第3个版本,即YOLO、YOLO 9000、YOLO v3,检测效果,更准更强。

    YOLO v3的更多细节,可以参考YOLO的官网。

    图片

    YOLO是一句美国的俗语,You Only Live Once,你只能活一次,即人生苦短,及时行乐。

    本文主要分享,如何实现YOLO v3的算法细节,Keras框架。这是第3篇,网络,以DarkNet为基础。当然还有第4篇,至第n篇,毕竟,这是一个完整版 :)这篇略长。

    本文的GitHub源码:https://github.com/SpikeKing/keras-yolo3-detection

    欢迎关注,公众号 深度算法 (ID: DeepAlgorithm) ,了解更多技术!


    1. 网络

    在模型中,通过传入输入层image_input、每层的anchor数num_anchors//3和类别数num_classes,调用yolo_body方法,构建YOLO v3的网络model_body。其中,image_input的结构是(?, 416, 416, 3)。

    model_body = yolo_body(image_input, num_anchors // 3, num_classes)  # model

    在model_body中,最终的输入是image_input,最终的输出是3个矩阵的列表:

    [(?, 13, 13, 18), (?, 26, 26, 18), (?, 52, 52, 18)]

    YOLO v3的基础网络是DarkNet网络,将DarkNet网络中底层和中层的特征矩阵,通过卷积操作和多个矩阵的拼接操作,创建3个尺度的输出,即[y1, y2, y3]。

    def yolo_body(inputs, num_anchors, num_classes):    darknet = Model(inputs, darknet_body(inputs))        x, y1 = make_last_layers(darknet.output, 512, num_anchors * (num_classes + 5))    x = compose(        DarknetConv2D_BN_Leaky(256, (1, 1)),        UpSampling2D(2))(x)    x = Concatenate()([x, darknet.layers[152].output])    x, y2 = make_last_layers(x, 256, num_anchors * (num_classes + 5))    x = compose(        DarknetConv2D_BN_Leaky(128, (1, 1)),        UpSampling2D(2))(x)    x = Concatenate()([x, darknet.layers[92].output])    x, y3 = make_last_layers(x, 128, num_anchors * (num_classes + 5))    return Model(inputs, [y1, y2, y3])


    2. Darknet

    Darknet网络的输入是图片数据集inputs,即(?, 416, 416, 3),输出是darknet_body方法的输出。将网络的核心逻辑封装在darknet_body方法中。即:

    darknet = Model(inputs, darknet_body(inputs))

    其中,darknet_body的输出格式是(?, 13, 13, 1024)。

    Darknet的网络简化图,如下:

    图片

    网络简化图

    YOLO v3所使用的Darknet版本是Darknet53。那么,为什么是Darknet53呢?因为Darknet53是53个卷积层和池化层的组合,与Darknet简化图一一对应,即:

    53 = 2 + 1*2 + 1 + 2*2 + 1 + 8*2 + 1 + 8*2 + 1 + 4*2 + 1

    在darknet_body中,Darknet网络含有5组重复的resblock_body单元,即:

    def darknet_body(x):    '''Darknent body having 52 Convolution2D layers'''    x = DarknetConv2D_BN_Leaky(32, (3, 3))(x)    x = resblock_body(x, num_filters=64, num_blocks=1)    x = resblock_body(x, num_filters=128, num_blocks=2)    x = resblock_body(x, num_filters=256, num_blocks=8)    x = resblock_body(x, num_filters=512, num_blocks=8)    x = resblock_body(x, num_filters=1024, num_blocks=4)    return x

    在第1个卷积操作DarknetConv2D_BN_Leaky中,是3个操作的组合,即:

    • 1个Darknet的2维卷积Conv2D层,即DarknetConv2D;

    • 1个批正在化层,即BatchNormalization();

    • 1个LeakyReLU层,斜率是0.1,LeakyReLU是ReLU的变换;

    即:

    def DarknetConv2D_BN_Leaky(*args, **kwargs):    """Darknet Convolution2D followed by BatchNormalization and LeakyReLU."""    no_bias_kwargs = {'use_bias': False}    no_bias_kwargs.update(kwargs)    return compose(        DarknetConv2D(*args, **no_bias_kwargs),        BatchNormalization(),        LeakyReLU(alpha=0.1))

    其中,LeakyReLU的激活函数,如下:

    LeakyReLU

    其中,Darknet的2维卷积DarknetConv2D,具体操作如下:

    • 将核权重矩阵的正则化,使用L2正则化,参数是5e-4,即操作w参数;

    • Padding,一般使用same模式,只有当步长为(2,2)时,使用valid模式。避免在降采样中,引入无用的边界信息;

    • 其余参数不变,都与二维卷积操作Conv2D()一致;

    kernel_regularizer是将核权重参数w进行正则化,而BatchNormalization是将输入数据x进行正则化。

    实现:

    @wraps(Conv2D) def DarknetConv2D(*args, **kwargs):    """Wrapper to set Darknet parameters for Convolution2D."""    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)

    下一步,第1个残差结构resblock_body,输入的数据x是(?, 416, 416, 32),通道filters是64个,重复次数num_blocks是1次。第1个残差结构是网络简化图第1部分。

    x = resblock_body(x, num_filters=64, num_blocks=1)

    在resblock_body中,含有以下逻辑:

    • ZeroPadding2D:填充x的边界为0,由(?, 416, 416, 32)转换为(?, 417, 417, 32)。因为下一步卷积操作的步长为2,所以图的边长需要是奇数;

    • DarknetConv2D_BN_Leaky:DarkNet的2维卷积操作,核是(3,3),步长是(2,2),注意,这会导致特征尺寸变小,由(?, 417, 417, 32)转换为(?, 208, 208, 64)。由于num_filters是64,所以产生64个通道。

    • compose:输出预测图y,功能是组合函数,先执行1x1的卷积操作,再执行3x3的卷积操作,filter先降低2倍后恢复,最后与输入相同,都是64;

    • x = Add()([x, y]):残差操作,将x的值与y的值相加。残差操作可以避免,在网络较深时所产生的梯度弥散问题。

    实现:

    def resblock_body(x, num_filters, num_blocks):    '''A series of resblocks starting with a downsampling Convolution2D'''    # Darknet uses left and top padding instead of 'same' mode    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 = compose(            DarknetConv2D_BN_Leaky(num_filters // 2, (1, 1)),            DarknetConv2D_BN_Leaky(num_filters, (3, 3)))(x)        x = Add()([x, y])    return x

    残差操作流程,如图:

    图片

    Residual

    同理,在darknet_body中,执行5组resblock_body残差块,重复[1, 2, 8, 8, 4]次,双卷积操作,每组均含有一次步长为2的卷积操作,因而一共降维5次32倍,即32=2^5,则输出的特征图维度是13,即13=416/32。最后1层的通道数是1024,因此,最终的输出结构是(?, 13, 13, 1024),即:

    Tensor("add_23/add:0", shape=(?, 13, 13, 1024), dtype=float32)

    至此,Darknet模型的输入是(?, 416, 416, 3),输出是(?, 13, 13, 1024)。


    3. 特征图

    在YOLO v3网络中,输出3个不同尺度的检测图,用于检测不同大小的物体。调用3次make_last_layers,产生3个检测图,即y1、y2和y3。

    13x13检测图

    第1个部分,输出维度是13x13。在make_last_layers方法中,输入参数如下:

    • darknet.output:DarkNet网络的输出,即(?, 13, 13, 1024);

    • num_filters:通道个数512,用于生成中间值x,x会传导至第2个检测图;

    • out_filters:第1个输出y1的通道数,值是锚框数*(类别数+4个框值+框置信度);

    即:

    x, y1 = make_last_layers(darknet.output, 512, num_anchors * (num_classes + 5))

    在make_last_layers方法中,执行2步操作:

    • 第1步,x执行多组1x1的卷积操作和3x3的卷积操作,filter先扩大再恢复,最后与输入的filter保持不变,仍为512,则x由(?, 13, 13, 1024)转变为(?, 13, 13, 512);

    • 第2步,x先执行3x3的卷积操作,再执行不含BN和Leaky的1x1的卷积操作,作用类似于全连接操作,生成预测矩阵y;

    实现:

    def make_last_layers(x, num_filters, out_filters):    '''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer'''    x = compose(        DarknetConv2D_BN_Leaky(num_filters, (1, 1)),        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),        DarknetConv2D_BN_Leaky(num_filters, (1, 1)),        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),        DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x)    y = compose(        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),        DarknetConv2D(out_filters, (1, 1)))(x)    return x, y

    最终,第1个make_last_layers方法,输出的x是(?, 13, 13, 512),输出的y是(?, 13, 13, 18)。由于模型只有1个检测类别,因而y的第4个维度是18,即3*(1+5)=18。

    26x26检测图

    第2个部分,输出维度是26x26,包含以下步骤:

    • 通过DarknetConv2D_BN_Leaky卷积,将x由512的通道数,转换为256的通道数;

    • 通过2倍上采样UpSampling2D,将x由13x13的结构,转换为26x26的结构;

    • 将x与DarkNet的第152层拼接Concatenate,作为第2个make_last_layers的输入,用于生成第2个预测图y2;

    其中,输入的x和darknet.layers[152].output的结构都是26x26的尺寸,如下:

    x: shape=(?, 26, 26, 256) darknet.layers[152].output: (?, 26, 26, 512)

    在拼接之后,输出的x的格式是(?, 26, 26, 768)。

    这样做的目的是:将Darknet最底层的高级抽象信息darknet.output,经过若干次转换之后,除了输出给第1个检测部分,还被用于第2个检测部分,经过上采样,与Darknet骨干中,倒数第2次降维的数据拼接,共同作为第2个检测部分的输入。底层抽象特征含有全局信息,中层抽象特征含有局部信息,这样拼接,两者兼顾,用于检测较小的物体。

    最后,还是调用相同的make_last_layers,输出第2个检测层y2和临时数据x。

    实现:

    x = compose(    DarknetConv2D_BN_Leaky(256, (1, 1)),    UpSampling2D(2))(x) x = Concatenate()([x, darknet.layers[152].output]) x, y2 = make_last_layers(x, 256, num_anchors * (num_classes + 5))

    最终,第2个make_last_layers方法,输出的x是(?, 26, 26, 256),输出的y是(?, 26, 26, 18)。

    52x52检测图

    第3个部分,输出维度是52x52,与第2个部分类似,包含以下步骤:

    x = compose(    DarknetConv2D_BN_Leaky(128, (1, 1)),    UpSampling2D(2))(x) x = Concatenate()([x, darknet.layers[92].output]) _, y3 = make_last_layers(x, 128, num_anchors * (num_classes + 5))

    逻辑如下:

    • x经过128个filter的卷积,再执行上采样,输出为(?, 52, 52, 128);

    • darknet.layers[92].output,与152层类似,结构是(?, 52, 52, 256);

    • 两者拼接之后,x是(?, 52, 52, 384);

    • 最后输入至make_last_layers,生成y3是(?, 52, 52, 18),忽略x的输出;

    最后,则是根据整个逻辑的输入和输出,构建模型。输入inputs依然保持不变,即(?, 416, 416, 3),而输出则转换为3个尺度的预测层,即[y1, y2, y3]。

    return Model(inputs, [y1, y2, y3])

    [y1, y2, y3]的结构如下:

    Tensor("conv2d_59/BiasAdd:0", shape=(?, 13, 13, 18), dtype=float32) Tensor("conv2d_67/BiasAdd:0", shape=(?, 26, 26, 18), dtype=float32) Tensor("conv2d_75/BiasAdd:0", shape=(?, 52, 52, 18), dtype=float32)

    最终,在yolo_body中,完成整个YOLO v3网络的构建,基础网络是DarkNet。

    model_body = yolo_body(image_input, num_anchors // 3, num_classes)

    网络的示意图,层次序号略有不同:

    图片

    网络的示意图

    补充1. 卷积Padding

    在卷积操作中,针对于边缘数据,有两种操作,一种是舍弃valid,一种是填充same。

    如:

    数据:1 2 3 4 5 6 7 8 9 10 11 12 13 输入数据 = 13 过滤器宽度 = 6 步长 = 5

    第1种,valid操作,宽度是6,步长是5,执行数据:

    1 2 3 4 5 6 6 7 8 9 10 11 11 12 13(不足,舍弃)

    第2种,same操作,执行数据:

    1 2 3 4 5 6(前两步相同) 6 7 8 9 10 11 11 12 13 0 0(不足,填充)

    其中,same模式中数据利用率更高,valid模式中避免引入无效的边缘数据,两种模式各有千秋。


    补充2. compose函数

    compose函数,使用Python的Lambda表达式,顺次执行函数列表,且前一个函数的输出是后一个函数的输入。compose函数适用于在神经网络中连接两个层。

    例如:

    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.') def func_x(x):    return x * 10 def func_y(y):    return y - 6 z = compose(func_x, func_y)  # 先执行x函数,再执行y函数 print(z(10))  # 10*10-6=94


    补充3. UpSampling2D上采样

    UpSampling2D上采样操作,将特征矩阵按倍数扩大,其核心是通过resize的方式,默认使用最邻近(Nearest Neighbor)插值算法。data_format是数据模式,默认是channels_last,即通道在最后,如(128,128,3)。

    源码:

    def call(self, inputs):    return K.resize_images(inputs, self.size[0], self.size[1],                           self.data_format) // ... x = tf.image.resize_nearest_neighbor(x, new_shape)  

    例如:数据(?, 13, 13, 256),经过上采样2倍操作,即UpSampling2D(2),生成(?, 26, 26, 256)的特征图。


    补充4. 1x1卷积操作与全连接

    1x1的卷积层和全连接层都可以作为最后一层的预测输出,两者之间略有不同。

    第1点:

    • 1x1的卷积层,可以不考虑输入的通道数,输出固定通道数的特征矩阵;

    • 全连接层(Dense),输入和输出都是固定的,在设计网络时,固定就不能修改;

    这样,1x1的卷积层,比全连接层,更为灵活;

    第2点:

    例如:输入(13,13,1024),输出为(13,13,18),则两种操作:

    • 1x1的卷积层,参数较少,只需与输出通道匹配的参数,如13x13x1x1x18个参数;

    • 全连接层,参数较多,需要与输入和输出都匹配的参数,如13x13x1028x18个参数;


    OK, that's all! Enjoy it!

    By C. L. Wang @ 美图云 视觉技术部

  • 相关阅读:
    分享一组很赞的 jQuery 特效【附源码下载】
    HTML5 Canvas 实现的9个 Loading 效果
    Charted – 自动化的可视化数据生成工具
    jQuery Countdown Timer 倒计时效果
    Droidicon – 1600+ 漂亮的 Android 图标
    tiltShift.js
    推荐12个最好的 JavaScript 图形绘制库
    Elastic Image Slider 带缩略图功能的幻灯片
    12款高质量的响应式 HTML5/CSS3 网站模板
    7种鼠标悬停效果,多样的图片说明展示
  • 原文地址:https://www.cnblogs.com/shuimuqingyang/p/14132258.html
Copyright © 2011-2022 走看看