从零开始实现YOLO V3——day2YOLO V3模型拆解

两阶段的目标检测算法,如R-CNN系列算法需要先产生候选区域,再对候选区域做分类和位置坐标的预测。近几年,很多研究人员相继提出一系列单阶段的检测算法如YOLO系列,SSD算法等,只需要一个网络即可同时产生候选区域并预测出物体的类别和位置坐标。

与R-CNN系列算法不同,YOLOv3使用单个网络结构,在产生候选区域的同时即可预测出物体类别和位置,不需要分成两阶段来完成检测任务。另外,YOLOv3算法产生的预测框数目比Faster R-CNN少很多。Faster R-CNN中每个真实框可能对应多个标签为正的候选区域,而YOLOv3里面每个真实框只对应一个正的候选区域。这些特性使得YOLOv3算法具有更快的速度,能到达实时响应的水平。

Joseph Redmon等人在2015年提出YOLO(You Only Look Once,YOLO)算法,通常也被称为YOLOv1;2016年,他们对算法进行改进,又提出YOLOv2版本;2018年发展出YOLOv3版本。

YOLOv3模型设计思想

YOLOv3算法的基本思想可以分成两部分:

  • 按一定规则在图片上产生一系列的候选区域,然后根据这些候选区域与图片上物体真实框之间的位置关系对候选区域进行标注。跟真实框足够接近的那些候选区域会被标注为正样本,同时将真实框的位置作为正样本的位置目标。偏离真实框较大的那些候选区域则会被标注为负样本,负样本不需要预测位置或者类别。
  • 使用卷积神经网络提取图片特征并对候选区域的位置和类别进行预测。这样每个预测框就可以看成是一个样本,根据真实框相对它的位置和类别进行了标注而获得标签值,通过网络模型预测其位置和类别,将网络预测值和标签值进行比较,就可以建立起损失函数。

在这里插入图片描述

图1:YOLOv3算法预测流程图

YOLOv3算法预测过程的流程图如 图1 所示,预测图片经过一系列预处理(resize、normalization等)输入到YOLOv3模型,根据预先设定的Anchor和提取到的图片特征得到目标预测框,最后通过非极大值抑制(NMS)消除重叠较大的冗余预测框,得到最终预测结果。

在这里插入图片描述

图2:YOLOv3算法训练流程图

进一步的可以被等价于图2中中任务流程,首先基于卷积神经网络提取不同尺度的特征,然后结合图片上生成的锚框预测真实框的位置和大小,最后基于损失函数更新权重。

  • 图2 左边是输入图片,上半部分所示的过程是使用卷积神经网络对图片提取特征,随着网络不断向前传播,特征图的尺寸越来越小,每个像素点会代表更加抽象的特征模式,直到输出特征图,其尺寸减小为原图的 1 32 \frac{1}{32} 321
  • 图2 下半部分描述了生成候选区域的过程,首先将原图划分成多个小方块,每个小方块的大小是
    32×32,然后以每个小方块为中心分别生成一系列锚框,整张图片都会被锚框覆盖到。在每个锚框的基础上产生一个与之对应的预测框,根据锚框和预测框与图片上物体真实框之间的位置关系,对这些预测框进行标注。
  • 将上方支路中输出的特征图与下方支路中产生的预测框标签建立关联,创建损失函数,开启端到端的训练过程。

通用的视觉任务研发全流程

类比通用的视觉任务研发全流程,目标检测模型构建流程如图3所示,目标检测既需要识别出物体的类别,还需要标示出每个目标的位置,因此需要检测头结构,用于计算预测框是否包含物体的概率、预测框位置坐标以及物体属于每个类别的概率。同时在目标检测中,检测的物体形状尺寸可能变化比较大,使用单一尺寸的特征图做预测容易造成漏检,而且像素点包含的语义信息可能不够丰富,难以提取到有效的特征模式,因此在目标检测中将高级层的特征图尺寸放大之后跟低层级的特征图进行融合,从而检测多尺度目标。
这里的多尺度检测,就对应图2上方分支,使用卷积神经网络提取多个尺度下的特征进行融合。
在这里插入图片描述

图3:通用的视觉任务研发全流程
  • 数据处理:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取,同时产生候选区域;
  • 模型构建:设计目标检测网络结构;
  • 特征提取:使用卷积神经网络提取特征;
  • 检测头设计:计算预测框是否包含物体的概率、预测框位置坐标、物体属于每个类别概率;
  • 多尺度检测:将高级层的特征图尺寸放大之后跟低层级的特征图进行融合;
  • 损失函数:模型优化的目标;
  • 模型评估:在模型训练中或训练结束后岁模型进行评估测试,观察准确率;
  • 模型预测:使用训练好的模型进行测试,也需要准备数据和模型特征提取,最后对结果进行解析;
  • 模型部署:通常需要将训练好的模型在特定环境(服务器、手机等)中运行,即在设备端运行推理任务。

YOLO V3 的网络架构

在这里插入图片描述

图4:YOLOv3网络结构
  • Bockbone:骨干网络,主要用于特征提取
  • Neck:在Backbone和Head之间提取不同阶段中特征图
  • Head:检测头,用于预测目标的类别和位置

下面对网络结构进行拆解

YOLO V3 网络架构拆解

YOLO V3主要由3个模块组成:Bockbone:骨干网络,主要用于特征提取;Neck:在Backbone和Head之间提取不同阶段中特征图;Head:检测头,用于预测目标的类别和位置;

Bockbone

在图像分类中学习过了通过卷积神经网络提取图像特征。通过连续使用多层卷积和池化等操作,能得到语义含义更加丰富的特征图。在检测问题中,也使用卷积神经网络逐层提取图像特征,通过最终的输出特征图来表征物体位置和类别等信息。

YOLOv3算法使用的骨干网络是Darknet53。Darknet53网络的具体结构如 图5 所示,在ImageNet图像分类任务上取得了很好的成绩。在检测任务中,将图中C0后面的平均池化、全连接层和Softmax去掉,保留从输入到C0部分的网络结构,作为检测模型的基础网络结构,也称为骨干网络。YOLOv3模型会在骨干网络的基础上,再添加检测相关的网络模块。
在这里插入图片描述

图5:Darknet53网络结构
从图5可以看出Darknet53就是一个典型的卷积神经网络,并且他采取了resNet的残差结构。关于卷积神经网络的介绍可以查阅其它资料,这里直接给出Darknet53的实现。

由于自己的笔记本性能有限,这里采用百度的飞浆平台,如果是使用pytorch的小伙伴,将paddle换成torch就好了,基本语法一致。

导入深度学习框架,这里是paddle,你也可以是torch,tensorflow

import paddle
import paddle.nn.functional as F
import numpy as np

构建ConvBNLayer网络,ConvBNLayer是yolo_v3的基本组件。就是卷积+BN+Leaky relu。对于v3来说,BN和leaky relu已经是和卷积层不可分离的部分了(最后一层卷积除外),共同构成了最小组件。

class ConvBNLayer(paddle.nn.Layer):
    def __init__(self, ch_in, ch_out, 
                 kernel_size=3, stride=1, groups=1,
                 padding=0, act="leaky"):
        super(ConvBNLayer, self).__init__()
    	#卷积操作
        self.conv = paddle.nn.Conv2D(
            in_channels=ch_in,
            out_channels=ch_out,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding,
            groups=groups,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.Normal(0., 0.02)),
            bias_attr=False)
    	#批量归一化操作
        self.batch_norm = paddle.nn.BatchNorm2D(
            num_features=ch_out,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.Normal(0., 0.02),
                regularizer=paddle.regularizer.L2Decay(0.)),
            bias_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.Constant(0.0),
                regularizer=paddle.regularizer.L2Decay(0.)))
        #判断是否需要使用leaky
        self.act = act
     
    def forward(self, inputs):
        out = self.conv(inputs)
        out = self.batch_norm(out)
        if self.act == 'leaky':
            out = F.leaky_relu(x=out, negative_slope=0.1)
        return out

构建下采样DownSample网络块

class DownSample(paddle.nn.Layer):
    # 下采样,图片尺寸减半,具体实现方式是使用stirde=2的卷积
    def __init__(self,
                 ch_in,
                 ch_out,
                 kernel_size=3,
                 stride=2,
                 padding=1):

        super(DownSample, self).__init__()

        self.conv_bn_layer = ConvBNLayer(
            ch_in=ch_in,
            ch_out=ch_out,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding)
        self.ch_out = ch_out
    def forward(self, inputs):
        out = self.conv_bn_layer(inputs)
        return out

构建基于ConvBNLayer的残差单元块,实现两个ConvBNLayer之间的残差链接

class BasicBlock(paddle.nn.Layer):
    """
    基本残差单元的定义,输入x经过两个ConvBNLayer,然后接第个ConvBNLayer的输出和输入x相加
    """
    def __init__(self, ch_in, ch_out):
        super(BasicBlock, self).__init__()

        self.conv1 = ConvBNLayer(
            ch_in=ch_in,
            ch_out=ch_out,
            kernel_size=1,
            stride=1,
            padding=0
            )
        self.conv2 = ConvBNLayer(
            ch_in=ch_out,
            ch_out=ch_out*2,
            kernel_size=3,
            stride=1,
            padding=1
            )
    def forward(self, inputs):
        conv1 = self.conv1(inputs)
        conv2 = self.conv2(conv1)
        out = paddle.add(x=inputs, y=conv2)
        return out

注意上面定义的是一个残差单元操作。图5中左边的不同残差块n,n表示这个残差块里含有多少个残差单元。这是yolo_v3的大组件,yolo_v3开始借鉴了ResNet的残差结构,使用这种结构可以让网络结构更深(从v2的darknet-19上升到v3的darknet-53,前者没有残差结构)。

下面实现残差块n的的操作,定义一个LayerWarp网络,可以添加多层残差块,组成Darknet53网络的一个层级。

class LayerWarp(paddle.nn.Layer):
    """
    添加多层残差块,组成Darknet53网络的一个层级
    """
    #通过传入count值,定义需要几个残差连接单元BasicBlock块
    def __init__(self, ch_in, ch_out, count, is_test=True):
        super(LayerWarp,self).__init__()
		
        self.basicblock0 = BasicBlock(ch_in,
            ch_out)
        self.res_out_list = []
        for i in range(1, count):
            res_out = self.add_sublayer("basic_block_%d" % (i), # 使用add_sublayer添加子层
                BasicBlock(ch_out*2,
                    ch_out))
            self.res_out_list.append(res_out)

    def forward(self,inputs):
        y = self.basicblock0(inputs)
        for basic_block_i in self.res_out_list:
            y = basic_block_i(y)
        return y
        

根据YOLO V3的网络架构图可以得出DarkNet 每组残差块的个数,如下所示。

# DarkNet 每组残差块的个数,来自DarkNet的网络结构图
DarkNet_cfg = {
   53: ([1, 2, 8, 8, 4])}

根据DarkNet53的网络模型,将上述各个子网络模块组合起来,就能得到一个完整的DarkNet53特征提取网络。

class DarkNet53_conv_body(paddle.nn.Layer):
    def __init__(self):
        super(DarkNet53_conv_body, self).__init__()
        self.stages = DarkNet_cfg[53]
        #DarkNet53的各个残差层
        self.stages = self.stages[0:5]
        # 第一层卷积
        self.conv0 = ConvBNLayer(
            ch_in=3,
            ch_out=32,
            kernel_size=3,
            stride=1,
            padding=1)
        # 下采样,使用stride=2的卷积来实现
        self.downsample0 = DownSample(
            ch_in=32,
            ch_out=32 * 2)
        # 添加各个层级的实现
        self.darknet53_conv_block_list = []
        self.downsample_list = []
        for i, stage in enumerate(self.stages):
            conv_block = self.add_sublayer(
                "stage_%d" % (i),
                LayerWarp(32*(2**(i+1)),
                32*(2**i),
                stage))
            self.darknet53_conv_block_list.append(conv_block)
        # 两个层级之间使用DownSample将尺寸减半
        for i in range(len(self.stages) - 1):
            downsample = self.add_sublayer(
                "stage_%d_downsample" % i,
                DownSample(ch_in=32*(2**(i+1)),
                    ch_out=32*(2**(i+2))))
            self.downsample_list.append(downsample)

    def forward(self,inputs):
        out = self.conv0(inputs)
        #print("conv1:",out.numpy())
        out = self.downsample0(out)
        #print("dy:",out.numpy())
        blocks = []
        for i, conv_block_i in enumerate(self.darknet53_conv_block_list): #依次将各个层级作用在输入上面
            out = conv_block_i(out)
            blocks.append(out)
            if i < len(self.stages) - 1:
                out = self.downsample_list[i](out)
        return blocks[-1:-4:-1] # 将C0, C1, C2作为返回值
        

基于上述Darknet53骨干网络的实现代码,将图5中的C0、C1、C2所表示的输出数据取出,并查看它们的形状分别是否是C0[1,1024,20,20],C1[1,512,40,40],C2[1,256,80,80]。

# 查看Darknet53网络输出特征图
import numpy as np
backbone = DarkNet53_conv_body()
x = np.random.randn(1, 3, 640, 640).astype('float32')
x = paddle.to_tensor(x)
C0, C1, C2 = backbone(x)
print(C0.shape, C1.shape, C2.shape)

程序输出

[1, 1024, 20, 20] [1, 512, 40, 40] [1, 256, 80, 80]

这说明我们的Darknet53网络搭建正确。

Neck(多尺度检测)

上面我们分析过在目标检测中,检测的物体形状尺寸可能变化比较大,使用单一尺寸的特征图做预测容易造成漏检,而且像素点包含的语义信息可能不够丰富,难以提取到有效的特征模式。

例如如果只在在特征图C0的基础上进行的,它的步幅stride=32。特征图的尺寸比较小,像素点数目比较少,每个像素点的感受野很大,具有非常丰富的高层级语义信息,可能比较容易检测到较大的目标。为了能够检测到尺寸较小的那些目标,需要在尺寸较大的特征图上面建立预测输出。如果我们在C2或者C1这种层级的特征图上直接产生预测输出,可能面临新的问题,它们没有经过充分的特征提取,像素点包含的语义信息不够丰富,有可能难以提取到有效的特征模式。在目标检测中,解决这一问题的方式是,将高层级的特征图尺寸放大之后跟低层级的特征图进行融合,得到的新特征图既能包含丰富的语义信息,又具有较多的像素点,能够描述更加精细的结构。

具体的网络实现方式如下图所示:
在这里插入图片描述

图6:生成多层级的输出特征图P0、P1、P2

YOLOv3在每个区域的中心位置产生3个锚框,在3个层级的特征图上产生锚框的大小分别为P2 [(10×13),(16×30),(33×23)],P1 [(30×61),(62×45),(59× 119)],P0[(116 × 90), (156 × 198), (373 × 326]。越往后的特征图上用到的锚框尺寸也越大,能捕捉到大尺寸目标的信息;越往前的特征图上锚框尺寸越小,能捕捉到小尺寸目标的信息。

在实际代码中,对于不同尺度的输入(P2即特征提取最初始的层不需要)需要使用concat连接将不同尺度的特征拼接到一起。在执行concat操作之前,需要将下层特征图进行上采样,变成和上层特征图同样的尺度。

定义一个上采样块

# 定义上采样模块
class Upsample(paddle.nn.Layer):
    def __init__(self, scale=2):
        super(Upsample,self).__init__()
        self.scale = scale

    def forward(self, inputs):
        # get dynamic upsample output shape
        shape_nchw = paddle.shape(inputs)
        shape_hw = paddle.slice(shape_nchw, axes=[0], starts=[2], ends=[4])
        shape_hw.stop_gradient = True
        in_shape = paddle.cast(shape_hw, dtype='int32')
        out_shape = in_shape * self.scale
        out_shape.stop_gradient = True

        # reisze by actual_shape
        out = paddle.nn.functional.interpolate(
            x=inputs, scale_factor=self.scale, mode="NEAREST")
        return out

上面我们得到C0[1,1024,20,20],C1[1,512,40,40],C2[1,256,80,80],假设我们需要将C0和C1做concat操作得到T1.

T1 = paddle.concat([Upsample(C0), C1], axis=1)

最后得到 T0[1,1024,40,40],从T1到图6中的P1还需要一系列的卷积操作,在后续代码中实现(回到检测头章节)。

检测头设计(计算预测框位置和类别)

上面我们说过检测头用于计算预测框是否包含物体的概率、预测框位置坐标、物体属于每个类别概率;

那么在计算预测框之前我们需要生成预测框,是的!我们似乎还没有预测框这个东西。回忆Day1中,我们了解了真实框,锚框,交并比的基本概念。我们下面要基于这些概念生成预测框。

产生候选区域(生成预测框)

如何产生候选区域,是检测模型的核心设计方案。目前大多数基于卷积神经网络的模型所采用的方式大体如下:

  • 按一定的规则在图片上生成一系列位置固定的锚框,将这些锚框看作是可能的候选区域。
  • 对锚框是否包含目标物体进行预测,如果包含目标物体,还需要预测所包含物体的类别,以及预测框相对于锚框位置需要调整的幅度。
生成锚框

将原始图片划分成m×n个区域,如下图所示,原始图片高度H=640, 宽度W=480,如果我们选择小块区域的尺寸为32×32,则m和n分别为:
m = 640 32 = 20 , n = 480 32 = 15 m = \frac{640}{32} = 20 \\\\ ,\\\\ n = \frac{480}{32} = 15 m=32640=20n=32480=15

在这里插入图片描述

图7:将图片划分成多个32x32的小方块

如图7所示,将原始图像分成了20行15列小方块区域。

回忆Day1中的锚框生成方式,不同于Day1中的以像素点为单位生成若干个锚框,YOLOv3算法会在每个区域的中心,生成一系列锚框。为了展示方便,我们先在图中第十行第四列的小方块位置附近画出生成的锚框(这里最上面的行号是第0行,最左边的列号是第0列)。如图8所示。
在这里插入图片描述

图8:在第10行第4列的小方块区域生成3个锚框

就这样在每个区域附近都生成3个锚框,就能覆盖整张图片。如图9所示.
在这里插入图片描述

图9:在每个小方块区域生成3个锚框
生成预测框

锚框的位置都是固定好的,不可能刚好跟物体边界框重合,需要在锚框的基础上进行位置的微调以生成预测框。预测框相对于锚框会有不同的中心位置和大小,采用什么方式能得到预测框呢?我们先来考虑如何生成其中心位置坐标。

比如上面图8中在第10行第4列的小方块区域中心生成的一个锚框,如绿色虚线框所示。以小方格的宽度为单位长度,
此小方块区域左上角的位置坐标是:
c x = 4 c y = 10 c_x=4 \\ c_y=10 cx=4cy=10
此锚框的区域中心坐标是:
c e n t e r x = c x + 0.5 = 4.5 c e n t e r y = c y + 0.5 = 10.5 center_x=c_x+0.5=4.5 \\ center_y=c_y +0.5=10.5 centerx=cx+0.5=4.5centery=cy+0.5=10.5
可以通过下面的方式生成预测框的中心坐标:
b x = c x + σ ( t x ) b y = c y + σ ( t y ) b_x=c_x+σ(t_x) \\ b_y=c_y+σ(t_y) bx=cx+σ(tx)b

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Keras深度学习实战(15)——从零开始实现YOLO目标检测是一篇非常实用的教程。YOLO(You Only Look Once)是一种流行的实时目标检测算法,其核心思想是将目标检测任务视为回归问题,并通过卷积神经网络实现端到端的检测。这篇教程提供了一步一步的实现代码,让读者能够快速了解并实践YOLO目标检测的方法。 首先,教程介绍了YOLO的工作原理和网络结构。YOLO将输入图像划分为多个网格,每个网格负责预测包含在该网格中的目标。每个网格预测包含目标的方框的位置和类别,以及目标的置信度。 接下来,教程详细介绍了如何实现YOLO的网络结构。使用Keras库,创建了一个具有卷积和池化层的卷积神经网络。还使用了Anchor Boxes,用来预测不同比例和宽高比的目标。 教程还介绍了如何预处理输入图像,包括将图像调整为适当的大小,并将目标边界框转换为YOLO需要的格式。然后,选择了合适的损失函数,训练了模型,以及进行了模型评估和预测。 最后,教程提供了一些改进和扩展的思路,包括使用更大的数据集进行训练、调整网络结构和超参数等等。 通过这篇教程,读者可以了解到YOLO目标检测的基本原理和实现步骤。并且,使用Keras库可以很方便地实现和训练自己的YOLO模型。无论是对于已经有一定深度学习基础的读者,还是对于刚刚开始学习的读者,这篇教程都是非常有价值的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值