paddle学习笔记——CV(2)YOLO-V3


前言

上一篇传送门

本篇文章尽量详细的说明YOLOV3算法的原理以及paddle实现。
本文中用到的图、数据、代码等都来自 这里

生成锚框和候选区域

  • 将原图划分成多个小方块
    原图大小:640 × 480
    方块尺寸: 32 × 32
    于是得到 20 × 15 个小方块
  • 每个方块内设置3个锚框,锚框的宽高可由聚类算法算得
    3种[w, h]:[116, 90], [156, 198], [373, 326]
    于是我们得到了 20 × 15 ×3 = 900 个anchor box
    如下图所示,每个红色格子内都有3锚框(我只画了其中两个格子的, 蓝色的锚框【尺寸画的不一定准】)
    在这里插入图片描述
  • 预测框
    预测框可以看作在锚框的基础上做微调。
    YOLOV3用如下的公式实现在锚框的基础上微调产生预测框中心坐标 [bx, by] 和宽高 [bw, bh]
    在这里插入图片描述
    这样做就取消了对网络预测值范围的限制。否则就是一个带约束条件求极值的问题。

标注候选区域

网络输出为任意值,经过锚框和上图公式转化为预测框。
接下来就是如何将标注数据转化为网络所需要的数据格式。

标注objectness标签

标注过程总结如下:
在这里插入图片描述
所以,某个方块的某个锚框的格式应该是(bx, by, bw, bh, objectness, c)其中c可以用one_hot格式
如果真实框在这个锚框内,那么它的标注数据应该是(*bx, *by, *bw, *bh, 1, c)
否则应该是(?, ?, ?, ?, 0 or -1, ?), ?代表无需标注,即我们并不关心?的值是什么,到时候做损失的时候不使用这个数据。
代码实现:
代码中每有体现出 objectness = -1 ???

# 标注预测框的objectness
def get_objectness_label(img, gt_boxes, gt_labels, iou_threshold = 0.7,
                         anchors = [116, 90, 156, 198, 373, 326],
                         num_classes=7, downsample=32):
    """
    img 是输入的图像数据,形状是[N, C, H, W]
    gt_boxes,真实框,维度是[N, 50, 4],其中50是真实框数目的上限,当图片中真实框不足50个时,不足部分的坐标全为0
              真实框坐标格式是xywh,这里使用相对值
    gt_labels,真实框所属类别,维度是[N, 50]
    iou_threshold,当预测框与真实框的iou大于iou_threshold时不将其看作是负样本
    anchors,锚框可选的尺寸
    anchor_masks,通过与anchors一起确定本层级的特征图应该选用多大尺寸的锚框
    num_classes,类别数目
    downsample,特征图相对于输入网络的图片尺寸变化的比例
    """

    img_shape = img.shape
    batchsize = img_shape[0]
    num_anchors = len(anchors) // 2
    input_h = img_shape[2]
    input_w = img_shape[3]
    # 将输入图片划分成num_rows x num_cols个小方块区域,每个小方块的边长是 downsample
    # 计算一共有多少行小方块
    num_rows = input_h // downsample
    # 计算一共有多少列小方块
    num_cols = input_w // downsample

    label_objectness = np.zeros([batchsize, num_anchors, num_rows, num_cols])
    label_classification = np.zeros([batchsize, num_anchors, num_classes, num_rows, num_cols])
    label_location = np.zeros([batchsize, num_anchors, 4, num_rows, num_cols])

    scale_location = np.ones([batchsize, num_anchors, num_rows, num_cols])

    # 对batchsize进行循环,依次处理每张图片
    for n in range(batchsize):
        # 对图片上的真实框进行循环,依次找出跟真实框形状最匹配的锚框
        for n_gt in range(len(gt_boxes[n])):
            gt = gt_boxes[n][n_gt]
            gt_cls = gt_labels[n][n_gt]
            gt_center_x = gt[0]
            gt_center_y = gt[1]
            gt_width = gt[2]
            gt_height = gt[3]
            if (gt_height < 1e-3) or (gt_height < 1e-3):
                continue
            i = int(gt_center_y * num_rows)
            j = int(gt_center_x * num_cols)
            ious = []
            for ka in range(num_anchors):
                bbox1 = [0., 0., float(gt_width), float(gt_height)]
                anchor_w = anchors[ka * 2]
                anchor_h = anchors[ka * 2 + 1]
                bbox2 = [0., 0., anchor_w/float(input_w), anchor_h/float(input_h)]
                # 计算iou
                iou = box_iou_xywh(bbox1, bbox2)
                ious.append(iou)
            ious = np.array(ious)
            inds = np.argsort(ious)
            k = inds[-1]
            label_objectness[n, k, i, j] = 1
            c = gt_cls
            label_classification[n, k, c, i, j] = 1.

            # for those prediction bbox with objectness =1, set label of location
            dx_label = gt_center_x * num_cols - j
            dy_label = gt_center_y * num_rows - i
            dw_label = np.log(gt_width * input_w / anchors[k*2])
            dh_label = np.log(gt_height * input_h / anchors[k*2 + 1])
            label_location[n, k, 0, i, j] = dx_label
            label_location[n, k, 1, i, j] = dy_label
            label_location[n, k, 2, i, j] = dw_label
            label_location[n, k, 3, i, j] = dh_label
            # scale_location用来调节不同尺寸的锚框对损失函数的贡献,作为加权系数和位置损失函数相乘
            scale_location[n, k, i, j] = 2.0 - gt_width * gt_height

    # 目前根据每张图片上所有出现过的gt box,都标注出了objectness为正的预测框,剩下的预测框则默认objectness为0
    # 对于objectness为1的预测框,标出了他们所包含的物体类别,以及位置回归的目标
    return label_objectness.astype('float32'), label_location.astype('float32'), label_classification.astype('float32'), \
             scale_location.astype('float32')

图像特征提取

YOLO-V3中用Darknet53提取特征。下面这张图比较清晰的展示了YOLOv3的结构,其结构来自于 这里
在这里插入图片描述
这张图来自这里:

https://blog.csdn.net/leviopku/article/details/82660381

代码实现

# YOLO-V3骨干网络结构Darknet53的实现代码

class ConvBNLayer(fluid.dygraph.Layer):
    """
    卷积 + 批归一化,BN层之后激活函数默认用leaky_relu
    """
    def __init__(self,
                 ch_in,
                 ch_out,
                 filter_size=3,
                 stride=1,
                 groups=1,
                 padding=0,
                 act="leaky",
                 is_test=True):
        super(ConvBNLayer, self).__init__()

        self.conv = Conv2D(
            num_channels=ch_in,
            num_filters=ch_out,
            filter_size=filter_size,
            stride=stride,
            padding=padding,
            groups=groups,
            param_attr=ParamAttr(
                initializer=fluid.initializer.Normal(0., 0.02)),
            bias_attr=False,
            act=None)

        self.batch_norm = BatchNorm(
            num_channels=ch_out,
            is_test=is_test,
            param_attr=ParamAttr(
                initializer=fluid.initializer.Normal(0., 0.02),
                regularizer=L2Decay(0.)),
            bias_attr=ParamAttr(
                initializer=fluid.initializer.Constant(0.0),
                regularizer=L2Decay(0.)))
        self.act = act

    def forward(self, inputs):
        out = self.conv(inputs)
        out = self.batch_norm(out)
        if self.act == 'leaky':
            out = fluid.layers.leaky_relu(x=out, alpha=0.1)
        return out

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

        super(DownSample, self).__init__()

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

class BasicBlock(fluid.dygraph.Layer):
    """
    基本残差块的定义,输入x经过两层卷积,然后接第二层卷积的输出和输入x相加
    """
    def __init__(self, ch_in, ch_out, is_test=True):
        super(BasicBlock, self).__init__()

        self.conv1 = ConvBNLayer(
            ch_in=ch_in,
            ch_out=ch_out,
            filter_size=1,
            stride=1,
            padding=0,
            is_test=is_test
            )
        self.conv2 = ConvBNLayer(
            ch_in=ch_out,
            ch_out=ch_out*2,
            filter_size=3,
            stride=1,
            padding=1,
            is_test=is_test
            )
    def forward(self, inputs):
        conv1 = self.conv1(inputs)
        conv2 = self.conv2(conv1)
        out = fluid.layers.elementwise_add(x=inputs, y=conv2, act=None)
        return out

class LayerWarp(fluid.dygraph.Layer):
    """
    添加多层残差块,组成Darknet53网络的一个层级
    """
    def __init__(self, ch_in, ch_out, count, is_test=True):
        super(LayerWarp,self).__init__()

        self.basicblock0 = BasicBlock(ch_in,
            ch_out,
            is_test=is_test)
        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,
                    is_test=is_test))
            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

DarkNet_cfg = {53: ([1, 2, 8, 8, 4])}

class DarkNet53_conv_body(fluid.dygraph.Layer):
    def __init__(self,
                 
                 is_test=True):
        super(DarkNet53_conv_body, self).__init__()
        self.stages = DarkNet_cfg[53]
        self.stages = self.stages[0:5]

        # 第一层卷积
        self.conv0 = ConvBNLayer(
            ch_in=3,
            ch_out=32,
            filter_size=3,
            stride=1,
            padding=1,
            is_test=is_test)

        # 下采样,使用stride=2的卷积来实现
        self.downsample0 = DownSample(
            ch_in=32,
            ch_out=32 * 2,
            is_test=is_test)

        # 添加各个层级的实现
        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,
                is_test=is_test))
            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)),
                    is_test=is_test))
            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作为返回值

当指定输入数据的形状是(1,3,640,640)时,则3个层级的输出特征图的形状分别是C0(1,1024,20,20),C1(1,1024,40,40)和C2(1,1024,80,80)。

关联特征图和候选区域

我门希望网络输出尺寸为[n, (c+5)*3, m, n]。必须将c0,c1,c2的通道数降低。
这里用一系列的卷积操作实现。
在这里插入图片描述
将[n, c, m, n]reshape成[n, num_anchors, c+5, m, n] 其中[ n , i, :, m, n]对应着坐标为(m, n)的小方块内的第i个预测框(预测框数=锚框数)
伪代码

outs = outs.reshape([n, num_anchors, c+5, m, n])
#得到objectness概率值
pred_objectness = sigmoid(outs[:, :, 4, :, :])
#得到类别概率
pred_classes = sigmoid(outs[:, :, 5:, :, :])

建立损失函数

objectness = -1 的锚框

在这里插入图片描述
将预测框中与真实框的IOU大于阈值但objectness != 1 的框的objectness标注为 -1 不参与损失计算。在锚框的基础之上进一步用预测框来这样做,可以加速收敛。

YOLOv3的损失函数

  • 表征是否包含目标物体的损失函数,通过pred_objectness和label_objectness计算。
loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(pred_objectness, label_objectness)
  • 表征物体位置的损失函数,通过pred_location和label_location计算。
pred_location_x = pred_location[:, :, 0, :, :]
pred_location_y = pred_location[:, :, 1, :, :]
pred_location_w = pred_location[:, :, 2, :, :]
pred_location_h = pred_location[:, :, 3, :, :]
loss_location_x = fluid.layers.sigmoid_cross_entropy_with_logits(pred_location_x, label_location_x)
loss_location_y = fluid.layers.sigmoid_cross_entropy_with_logits(pred_location_y, label_location_y)
# pred_location_y为任意值, label_location_y为sigmoid后的值
loss_location_w = fluid.layers.abs(pred_location_w - label_location_w)
loss_location_h = fluid.layers.abs(pred_location_h - label_location_h)
loss_location = loss_location_x + loss_location_y + loss_location_w + loss_location_h
  • 表征物体类别的损失函数,通过pred_classification和label_classification计算。
loss_obj = fluid.layers.sigmoid_cross_entropy_with_logits(pred_classification, label_classification)

只有当objectness=1时才计算物体位置的损失函数和类别损失;当objectness=0时,只计算表征是否包含目标物体的损失函数;当objectness=-1时,不计算过任何损失。

  • 读取数据
  • 标注锚框
  • 定义网络结构特征
  • 光亮特征图和候选区域
  • 标注objectness=-1
  • 建立三种损失函数:objectness, location, classification

多尺度检测

输入图片经过特征提取后得到C0, C1, C2,通过多尺度融合提取出P0, P1, P2。
C0stride大,方块数少,适合预测大尺度的目标;C2stride小,方块数多,适合预测小尺度的目标。
如果直接用C2经过简单的特征提取得到P2,效果可能不好,C2虽然stride小,但其网络层数少,语义信息不够丰富,若从更高层的网络经过upsample后与C2融合得到P2,效果会更好。
生成多层级的输出特征图P0、P1、P2:
在这里插入图片描述

这里得到P0, P1, P2的具体细节和 这张图 中的并不完全相同,具体细节还需要查看paddle的源码。

开启端到端的训练

在这里插入图片描述
输入图片经过特征提取后得到三个尺度的输出,将其转化为我们需要的格式;将真实框在在三个尺度上打标签,之后用它与输出做损失,整个过程比较复杂。paddle为我们提供了一个API可以方便的实现上述过程。

paddle.fluid.layers.yolov3_loss

  • x (Variable) –YOLOv3损失运算的输入张量,这是一个形状为[N,C,H,W]的四维Tensor。H和W应该相同,第二维(C)存储框的位置信息,以及每个anchor box的置信度得分和one-hot分类。数据类型为float32或float64。
  • gt_box (Variable) – 真实框,应该是[N,B,4]的形状。第三维用来承载x、y、w、h,其中 x, y是真实框的中心坐标,w, h是框的宽度和高度,且x、y、w、h将除以输入图片的尺寸,缩放到[0,1]区间内。 N是batch size,B是图像中所含有的的最多的box数目。数据类型为float32或float64。
  • gt_label (Variable) – 真实框的类id,应该形为[N,B]。数据类型为int32。
  • anchors (list|tuple) – 指定anchor框的宽度和高度,它们将逐对进行解析
  • anchor_mask (list|tuple) – 当前YOLOv3损失计算中使用anchor的mask索引
  • class_num (int) – 要预测的类别数
  • ignore_thresh (float) – 一定条件下忽略某框置信度损失的忽略阈值
  • downsample_ratio (int) – 网络输入到YOLOv3 loss输入的下采样率,因此第一,第二和第三个 loss
    的下采样率应分别为32,16,8
  • gt_score (Variable) - 真实框的混合得分,形为[N,B]。 默认None。数据类型为float32或float64。
  • use_label_smooth (bool) - 是否使用平滑标签。 默认为True
  • name (str|None) – 具体用法请参见 cn_api_guide_Name ,一般无需设置,默认值为None。

开启训练

伪代码

with fluid.dygraph.guard():
    model.YOLOv3() #加载模型
    lr = get_lr() #设置学习率
    opt = fluid.optimizer.Adam() #创建优化器
    #创建数据读取器
    train_loader = multithread_loader()
    MAX_EPOCH= 200
    model.train()
    for epoch in range(MAX_EPOCH):
        for i , data in enumerate(train_loader ()):
                img, gt_boxes, gt_labels, img_scale = data
                #优化技巧mixup需要, 此处没用mixup, gt——scores设置为1即可
                gt_scores = np.ones(gt_labels.shape).astype('float32')
                gt_scores = to_variable(gt_scores)
                img = to_variable(img)
                gt_boxes = to_variable(gt_boxes)
                gt_labels = to_variable(gt_labels)
                
                outputs = model(img)  #前向传播,输出[P0, P1, P2]
                loss = model.get_loss(...)# 计算损失函数

                loss.backward()     # 反向传播计算梯度
                opt.minimize(loss)  # 更新参数
                model.clear_gradients()

模型预测

现在只考虑一次只预测一张图的情形(实时监测肯定只能一张一张的预测啦)
预测框得分 = P(obj) * P(class)
总共可以得到(输入图片尺寸若是608 608)19193+38383+7676*3=22743个框,过滤掉小概率的框,使用多分类非极大值抑制消除重合的框,最后输出预测框和得分。
在这里插入图片描述
paddle中为我们提供了相应的API:

paddle.fluid.layers.yolo_box

  • x (Variable) - YoloBox的输入张量是一个4-D张量,形状为[N,C,H,W]。第二维(C)存储每个anchor box位置坐标,每个anchor box的置信度分数和one hot key。通常,X应该是YOLOv3网络的输出。数据类型为float32或float64
  • img_size (Variable) - YoloBox的图像大小张量,这是一个形状为[N,2]的二维张量。该张量保持每个输入图像的高度和宽度,用于对输出图像按输入图像比例调整输出框的大小。数据类型为int32。
  • anchors (list | tuple) - anchor的宽度和高度,它将逐对解析
  • class_num (int) - 要预测的类数
  • conf_thresh (float) - 检测框的置信度得分阈值。置信度得分低于阈值的框应该被忽略
  • downsample_ratio (int) - 从网络输入到YoloBox操作输入的下采样率,因此应依次为第一个,第二个和第三个YoloBox运算设置该值为32,16,8
  • clip_bbox (bool) - 是否将输出的bbox裁剪到 img_size 范围内,默认为True。
  • name (str|None) – 具体用法请参见 cn_api_guide_Name ,一般无需设置,默认值为None。

这个函数会返回置信度高于某一阈值的所有预测框

NMS

作用:把预测同一个物体的多个框去除,只保留置信度最高的那个框。
效果如下图:
在这里插入图片描述

预测过程

伪代码

test_loader = test_data_loader(...batch_size=1)
for img in test_loader():
    outputs = model.forward(img) # 输出[P0, P1, P2]
    box = get_pred(outputs)      # 输出预测框
    res = NMS(box)

最后输出结果保存成如下格式:
list( [ [ img1_name, [ [框1],[框2] ] ], [ img1_name, [ [框1],[框2] ] ][ img1_name, [ [框1],[框2] ] ]…] )

小结

在这里插入图片描述
至此,我们已经基本掌握了从数据处理到训练完输出预测结果的整个流程,对于每个步骤过程的具体实现还需要多多看源码,熟悉整个流程,感悟代码设计的精髓,赞叹算法代码实现的精妙,思考算法设计的原理。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值