YOLO v3源码解读

YOLO v3是一种非常强大的目标检测模型,YOLO是You Only Look Once的缩写,为什么说是You Only Look Once?R-CNN系列模型需要看两眼,一眼看物体位置,一眼看物体类别,相当于说求解分为两部分,回归和分类。而YOLO结合了这两部分,统一为回归问题,所以只需要看一眼。

本文内容主要在于探索keras-yolo3的源码,会侧重于网络结构的解读分析。

从yolo_body函数开始:

def yolo_body(inputs, num_anchors, num_classes):
    """Create YOLO_V3 model CNN body in Keras."""
    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])

该函数传入三个值,inputs是输入图片,结构是(?, 416, 416, 3);num_anchors是单个尺度下先验框个数,为3;num_classes就是分类数,本文假定分类数为1

YOLO v3将Darknet-53作为特征提取的主要网络结构,用到了大量的卷积计算。简单来说,图像上的卷积计算就是卷积核在图像上每移动一定步长,与图像的相应位置做加权求和,组成新的输出特征图。

先来看看卷积部分的代码:

@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)

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))

调用DarknetConv2D_BN_Leaky函数就会构建一个卷积层,例如:

DarknetConv2D_BN_Leaky(32, (3,3))
  • 这里产生的通道数是32,卷积核大小是3*3,步长默认为1。
  • no_bias_kwargs = {'use_bias': False} 不加偏置向量
  • darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)} 在权重上施加L2正则项,约束其数量级,防止过拟合
  • darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same' 如果设置步长为(2, 2),则使用valid模式,避免在降采样时引入无用的边界信息,valid是从filter全部在image里面时开始做卷积,不足舍弃,same是从filter的中心与image的边角重合时开始做卷积,不足填充
  • 卷积之后,加入BatchNormalization(BN层),做批标准化,其作用是:(1)加速收敛 (2)控制过拟合,可以少用或不用Dropout和正则 (3)降低网络对初始化权重不敏感 (4)允许使用较大的学习率
  • 最后加入激活函数LeakyReLU,第三象限的斜率设成0.1,抑制输出(特征值)小于0的神经元,具有了稀疏激活性(稀疏后的模型更容易挖掘相关特征,并且不容易过拟合),也不会出现使用ReLU导致神经元直接死亡的情况。
  • compose通过reduce,闭包和匿名函数实现各个函数的嵌套调用。

然后看darknet_body函数:

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

首先做一次卷积,然后是5个连续的残差结构单元:

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

以第一个残差结构单元为例,x是上一步卷积的输出,shape为(?, 416, 416, 32),64是产生的通道数,1是残差计算的重复次数。

  • x = ZeroPadding2D(((1,0),(1,0)))(x) 给x张量补零,((top_pad, bottom_pad), (left_pad, right_pad)),因为下一步卷积操作的步长为2,所以图的边长需要是奇数,否则采样不全。shape变为(?, 417, 417, 32)
  • x = DarknetConv2D_BN_Leaky(num_filters, (3,3), strides=(2,2))(x) 下采样,shape变为(?, 208, 208, 64)
  • DarknetConv2D_BN_Leaky(num_filters//2, (1,1)) 这是一步1*1卷积,降维的同时跨通道整合,后面再做一次卷积恢复为64个通道,不影响计算量的情况下增加网络深度(在inception结构中能够降低计算量)
  • x = Add()([x,y]) 残差(Residual)操作,将上面第二层卷积的输出x与y相加(相同shape相加,shape不变)。残差操作可以避免在网络较深时所产生的梯度弥散问题

接下来是剩下的4个残差结构单元,只是计算的重复次数增加,经过Darknet-53之后的输出维度为13*13*1024

之后,通过卷积计算输出3种不同尺度下的检测图来预测边界框,分别是13*13,26*26,52*52,大尺度用于检测小物体,小尺度检测大物体。另外,从v2开始借鉴Faster R-CNN使用了先验框(Anchor Boxes),下面是我对先验框的解读:

  • 以13*13检测图为例,该尺度下的输出维度是(?, 13, 13, 18),我们可以将其reshape成(?, 13, 13, 3,4+1+1)来看,3代表三种先验框,4代表预测的边界框值,其中一个1代表框置信度,另一个1代表类别概率序列的长度
  • 将13*13检测图分为13*13的格子,每个格子中心位置都配上3种不同宽高的先验框,模型通过训练改变每个先验框的xy偏移量(范围0~1)和宽高比(范围0~1来预测落在当前格子的物体边界框,因此该尺度下预测的边界框有13*13*3个

我们接着看yolo_body函数,其通过make_last_layers函数得到yolo层的输出:

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
  • 依旧是一连串的卷积,先得到一个13*13*512的特征图x,经最后的1*1卷积,得到13*13尺度下的输出y1,维度是(?, 13, 13, 18)
  • 对前面得到的x降维,再做一次2倍上采样,得到输出26*26*256, 然后与darknet的第152层的输出26*26*512做连接,得到26*26*768,再经make_last_layers函数得到26*26尺度下的输出y2,维度是(?, 26, 26, 18)。这里的连接用到了跨层跳跃连接,由于底层信息含有全局特征,中层信息含有局部特征,这种连接融合了全局与局部的特征,检测效果更好
  • 同理,在52*52尺度下得到输出y3,维度是(?, 52, 52, 18)

上图是YOLO v3的网络结构图。整个网络结构已经解读完了,接下来就是涉及loss的计算部分,有空再写。

参考:

https://mp.weixin.qq.com/s/hC4P7iRGv5JSvvPe-ri_8g

https://www.cnblogs.com/makefile/p/YOLOv3.html

点赞 分享

发表评论

共有 0 条评论