睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

学习前言

对YoloV3进行了重构,用tensorflow2进行了复现。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

; 源码下载

https://github.com/bubbliiiing/yolo3-tf2
喜欢的可以点个star噢。

YoloV3实现思路

一、整体结构解析

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
在学习YoloV3之前,我们需要对YoloV3所作的工作有一定的了解,这有助于我们后面去了解网络的细节。

整个YoloV3可以分为三个部分,分别是Darknet53,FPN以及Yolo Head

Darknet53可以被称作YoloV3的主干特征提取网络,输入的图片首先会在Darknet53里面进行 特征提取,提取到的特征可以被称作特征层, 是输入图片的特征集合。在主干部分,我们 获取了三个特征层进行下一步网络的构建,这三个特征层我称它为 有效特征层

FPN可以被称作YoloV3的加强特征提取网络,在主干部分获得的三个 有效特征层会在这一部分进行特征融合,特征融合的目的是结合不同尺度的特征信息。在FPN部分,已经获得的 有效特征层被用于继续提取特征。

Yolo Head是YoloV3的分类器与回归器,通过Darknet53和FPN,我们已经可以获得三个加强过的有效特征层,他们的shape分别为(52,52,128),(26,26,256),(13,13,512)。每一个特征层都有宽、高和通道数,此时我们可以将 特征图看作一个又一个特征点的集合每一个特征点都有通道数个特征。Yolo Head实际上所做的工作就是 对特征点进行判断,判断 特征点是否有物体与其对应

因此,整个YoloV3网络所作的工作就是 特征提取-特征加强-预测特征点对应的物体情况

; 二、网络结构解析

1、主干网络Darknet53介绍

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
YoloV3所使用的主干特征提取网络为Darknet53,它具有两个重要特点:
1、使用了 残差网络Residual,Darknet53中的残差卷积可以分为两个部分,主干部分是一次1X1的卷积和一次3X3的卷积;残差边部分不做任何处理,直接将主干的输入与输出结合。 整个YoloV3的主干部分都由残差卷积构成,上述所示的YoloV3整体结构里, Resblock_body后面的x几就代表在这个特征层部分,残差结构重复了几次。Resblock_body的代码如下,for训练里的就是残差结构:

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

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
残差网络的特点是 容易优化,并且能够通过增加相当的 深度来提高准确率。其内部的 残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。

2、Darknet53的 每一个DarknetConv2D后面都紧跟了BatchNormalization标准化与LeakyReLU部分。普通的ReLU是将所有的负值都设为零,Leaky ReLU则是给所有负值赋予一个非零斜率。以数学的方式我们可以表示为**:

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
DarknetConv2D_BN_Leaky的实现代码如下

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

整个主干实现代码为:

from functools import wraps

from tensorflow.keras.initializers import RandomNormal
from tensorflow.keras.layers import (Add, BatchNormalization, Conv2D, LeakyReLU,
                          ZeroPadding2D)
from tensorflow.keras.regularizers import l2
from utils.utils import compose

@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
    darknet_conv_kwargs = {'kernel_initializer' : RandomNormal(stddev=0.02), '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):
    no_bias_kwargs = {'use_bias': False}
    no_bias_kwargs.update(kwargs)
    return compose(
        DarknetConv2D(*args, **no_bias_kwargs),
        BatchNormalization(),
        LeakyReLU(alpha=0.1))

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

def darknet_body(x):

    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)
    feat1 = x

    x = resblock_body(x, 512, 8)
    feat2 = x

    x = resblock_body(x, 1024, 4)
    feat3 = x
    return feat1, feat2, feat3

2、构建FPN特征金字塔进行加强特征提取

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
在特征利用部分,YoloV3提取 多特征层进行目标检测,一共 提取三个特征层
三个特征层位于主干部分Darknet53的不同位置,分别位于 中间层,中下层,底层,三个特征层的 shape分别为(52,52,256)、(26,26,512)、(13,13,1024)。

在获得三个有效特征层后,我们利用这三个有效特征层进行FPN层的构建,构建方式为:

  1. 13x13x1024的特征层进行5次卷积处理,处理完后 利用YoloHead获得预测结果一部分用于进行上采样UmSampling2d后与26x26x512特征层进行结合,结合特征层的shape为(26,26,768)。
  2. 结合特征层再次进行5次卷积处理,处理完后 利用YoloHead获得预测结果一部分用于进行上采样UmSampling2d后与52x52x256特征层进行结合,结合特征层的shape为(52,52,384)。
  3. 结合特征层再次进行5次卷积处理,处理完后 利用YoloHead获得预测结果

特征金字塔可以将 不同shape的特征层进行特征融合,有利于 提取出更好的特征

from tensorflow.keras.layers import Concatenate, Input, Lambda, UpSampling2D
from tensorflow.keras.models import Model
from utils.utils import compose

from nets.darknet import DarknetConv2D, DarknetConv2D_BN_Leaky, darknet_body
from nets.yolo_training import yolo_loss

def make_five_conv(x, num_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)
    return x

def make_yolo_head(x, num_filters, out_filters):
    y = DarknetConv2D_BN_Leaky(num_filters*2, (3,3))(x)
    y = DarknetConv2D(out_filters, (1,1))(y)
    return y

def yolo_body(input_shape, anchors_mask, num_classes):
    inputs      = Input(input_shape)

    C3, C4, C5  = darknet_body(inputs)

    x   = make_five_conv(C5, 512)
    P5  = make_yolo_head(x, 512, len(anchors_mask[0]) * (num_classes+5))

    x   = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(x)

    x   = Concatenate()([x, C4])

    x   = make_five_conv(x, 256)
    P4  = make_yolo_head(x, 256, len(anchors_mask[1]) * (num_classes+5))

    x   = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(x)

    x   = Concatenate()([x, C3])

    x   = make_five_conv(x, 128)
    P3  = make_yolo_head(x, 128, len(anchors_mask[2]) * (num_classes+5))
    return Model(inputs, [P5, P4, P3])

3、利用Yolo Head获得预测结果

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
利用FPN特征金字塔, 我们可以获得三个加强特征,这三个加强特征的shape分别为(13,13,512)、(26,26,256)、(52,52,128),然后我们利用这三个shape的特征层传入Yolo Head获得预测结果。

Yolo Head本质上是一次3×3卷积加上一次1×1卷积,3×3卷积的作用是特征整合,1×1卷积的作用是调整通道数。

对三个特征层分别进行处理,假设我们预测是的VOC数据集,我们的输出层的shape分别为(13,13,75),(26,26,75),(52,52,75), 最后一个维度为75是因为该图是基于voc数据集的,它的类为20种,YoloV3针对每一个特征层的每一个特征点存在3个先验框,所以预测结果的通道数为3×25;
如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3×85
,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255)

其实际情况就是,输入N张416×416的图片,在经过多层的运算后,会输出三个shape分别为(N,13,13,255),(N,26,26,255),(N,52,52,255)的数据,对应每个图分为13×13、26×26、52×52的网格上3个先验框的位置。

实现代码如下:

from tensorflow.keras.layers import Concatenate, Input, Lambda, UpSampling2D
from tensorflow.keras.models import Model
from utils.utils import compose

from nets.darknet import DarknetConv2D, DarknetConv2D_BN_Leaky, darknet_body
from nets.yolo_training import yolo_loss

def make_five_conv(x, num_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)
    return x

def make_yolo_head(x, num_filters, out_filters):
    y = DarknetConv2D_BN_Leaky(num_filters*2, (3,3))(x)
    y = DarknetConv2D(out_filters, (1,1))(y)
    return y

def yolo_body(input_shape, anchors_mask, num_classes):
    inputs      = Input(input_shape)

    C3, C4, C5  = darknet_body(inputs)

    x   = make_five_conv(C5, 512)
    P5  = make_yolo_head(x, 512, len(anchors_mask[0]) * (num_classes+5))

    x   = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(x)

    x   = Concatenate()([x, C4])

    x   = make_five_conv(x, 256)
    P4  = make_yolo_head(x, 256, len(anchors_mask[1]) * (num_classes+5))

    x   = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(x)

    x   = Concatenate()([x, C3])

    x   = make_five_conv(x, 128)
    P3  = make_yolo_head(x, 128, len(anchors_mask[2]) * (num_classes+5))
    return Model(inputs, [P5, P4, P3])

三、预测结果的解码

1、什么是先验框

由网络我们可以获得三个特征层的预测结果,shape分别为:

  • (N,13,13,255)
  • (N,26,26,255)
  • (N,52,52,255)

N代表的是batch_size,就是输入图片的数量,我们可以忽略,但是后面的(52,52,255)、(26,26,255)、(13,13,255),就不可以忽略了。

每一个预测结果都有宽、高和通道数,宽、高里面是一个又一个特征点,那此时我们便可以想办法利用这些特征点,和原图进行结合。

我们再看看预测结果的特点,13X13,26X26,52X52,它是不是非常像三个等分的网格?如果我们将原图划分成对应13X13,26X26,52X52的部分,是不是整个特征层就以某种形式映射在原图上了。

事实上yolo系列就是这么做的,每一个有效特征层将整个图片分成与其 长宽对应的网格仔细看看这幅图,原图被划分成13×13的网格;然后从每个网格中心建立多个先验框,典型值是一个特征点三个先验框,这些框是网络预先设定好的框,网络的预测结果会判断这些框内是否包含物体,以及这个物体的种类。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

; 2、获得先验框后做什么

由网络我们可以获得三个特征层的预测结果,shape分别为:

  • (N,13,13,255)
  • (N,26,26,255)
  • (N,52,52,255)
    由于每一个网格点都具有三个先验框,所以上述的预测结果可以reshape为:
  • (N,13,13,3,85)
  • (N,26,26,3,85)
  • (N,52,52,3,85)

其中的85可以拆分为4+1+80,其中的4代表先验框的调整参数,1代表先验框内是否包含物体,80代表的是这个先验框的种类,由于coco分了80类,所以这里是80。如果YoloV3只检测两类物体,那么这个85就变为了4+1+2 = 7。

即85包含了4+1+80, 分别代表x_offset、y_offset、h和w、置信度、分类结果。

但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。

YoloV3的解码过程分为两步:

  • 将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是 预测框的中心
  • 然后 再利用 先验框和h、w结合 计算出预测框的宽高。这样就能得到整个预测框的位置了。

下图展示了YoloV3解码的过程:

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
实现代码如下,当调用DecodeBox时,就会进行解码:

def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image):

    box_yx = box_xy[..., ::-1]
    box_hw = box_wh[..., ::-1]
    input_shape = K.cast(input_shape, K.dtype(box_yx))
    image_shape = K.cast(image_shape, K.dtype(box_yx))

    if letterbox_image:

        new_shape = K.round(image_shape * K.min(input_shape/image_shape))
        offset  = (input_shape - new_shape)/2./input_shape
        scale   = input_shape/new_shape

        box_yx  = (box_yx - offset) * scale
        box_hw *= scale

    box_mins    = box_yx - (box_hw / 2.)
    box_maxes   = box_yx + (box_hw / 2.)
    boxes  = K.concatenate([box_mins[..., 0:1], box_mins[..., 1:2], box_maxes[..., 0:1], box_maxes[..., 1:2]])
    boxes *= K.concatenate([image_shape, image_shape])
    return boxes

def get_grid_anchors(feats, anchors):
    num_anchors = len(anchors)

    grid_shape = K.shape(feats)[1:3]

    anchors_tensor = K.reshape(tf.constant(anchors), [1, 1, num_anchors, 2])
    anchors_tensor = K.tile(anchors_tensor, [grid_shape[0], grid_shape[1], 1, 1])

    grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]), [grid_shape[0], 1, num_anchors, 1])
    grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]), [1, grid_shape[1], num_anchors, 1])

    grid_anchors = K.concatenate([K.cast(grid_x, K.dtype(feats)), K.cast(grid_y, K.dtype(feats)), K.cast(anchors_tensor, K.dtype(feats))])
    grid_anchors = K.reshape(grid_anchors, [-1, 4])

    grid_w  = K.ones_like(grid_x) * grid_shape[1]
    grid_h  = K.ones_like(grid_y) * grid_shape[0]
    grid_wh = K.concatenate([K.cast(grid_h, K.dtype(feats)), K.cast(grid_w, K.dtype(feats))])
    grid_wh = K.reshape(grid_wh, [-1, 2])
    return grid_anchors, grid_wh

def decode_anchors(reshape_outputs, grid_anchors, grid_wh, input_shape):

    box_xy          = (K.sigmoid(reshape_outputs[..., :2]) + grid_anchors[:, :2])/grid_wh
    box_wh          = K.exp(reshape_outputs[..., 2:4]) * grid_anchors[:, 2:] / K.cast(input_shape[::-1], K.dtype(reshape_outputs))

    box_confidence  = K.sigmoid(reshape_outputs[..., 4:5])
    box_class_probs = K.sigmoid(reshape_outputs[..., 5:])
    return box_xy, box_wh, box_confidence, box_class_probs

def DecodeBox(outputs,
            anchors,
            num_classes,
            input_shape,

            anchor_mask     = [[6, 7, 8], [3, 4, 5], [0, 1, 2]],
            max_boxes       = 100,
            confidence      = 0.5,
            nms_iou         = 0.3,
            letterbox_image = True):

    image_shape = K.reshape(outputs[-1],[-1])

    grid_anchors    = []
    grid_whs        = []
    reshape_outputs = []
    for l in range(len(outputs) - 1):
        grid_anchor, grid_wh = get_grid_anchors(outputs[l], anchors[anchor_mask[l]])
        grid_anchors.append(grid_anchor)
        grid_whs.append(grid_wh)

        reshape_outputs.append(K.reshape(outputs[l], [-1, num_classes + 5]))

    grid_anchors    = K.concatenate(grid_anchors, axis = 0)
    reshape_outputs = K.concatenate(reshape_outputs, axis = 0)
    grid_whs        = K.concatenate(grid_whs, axis = 0)

    box_xy, box_wh, box_confidence, box_class_probs = decode_anchors(reshape_outputs, grid_anchors, grid_whs, input_shape)

    boxes       = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image)

5、得分筛选与非极大抑制

得到最终的预测结果后还要进行 得分排序与非极大抑制筛选

得分筛选就是 筛选出得分满足confidence置信度的预测框。
非极大抑制就是 筛选出一定区域内属于同一种类得分最大的框。

得分筛选与非极大抑制的过程可以概括如下:
1、找出该图片中 得分大于门限函数的框。在进行 重合框筛选前就进行得分的筛选可以大幅度减少框的数量。
2、对 种类进行循环,非极大抑制的作用是 筛选出一定区域内属于同一种类得分最大的框,对种类进行循环可以 帮助我们对每一个类分别进行非极大抑制。
3、根据得分对该种类进行从大到小排序。
4、每次取出得分最大的框,计算 其与其它所有预测框的重合程度,重合程度过大的则剔除。

得分筛选与非极大抑制后的结果就可以用于绘制预测框了。

下图是经过非极大抑制的。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
下图是未经过非极大抑制的。
睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
实现代码为:
box_scores  = box_confidence * box_class_probs

mask             = box_scores >= confidence
max_boxes_tensor = K.constant(max_boxes, dtype='int32')
boxes_out   = []
scores_out  = []
classes_out = []
for c in range(num_classes):

    class_boxes      = tf.boolean_mask(boxes, mask[:, c])
    class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])

    nms_index = tf.image.non_max_suppression(class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=nms_iou)

    class_boxes         = K.gather(class_boxes, nms_index)
    class_box_scores    = K.gather(class_box_scores, nms_index)
    classes             = K.ones_like(class_box_scores, 'int32') * c

    boxes_out.append(class_boxes)
    scores_out.append(class_box_scores)
    classes_out.append(classes)
boxes_out      = K.concatenate(boxes_out, axis=0)
scores_out     = K.concatenate(scores_out, axis=0)
classes_out    = K.concatenate(classes_out, axis=0)

四、训练部分

1、计算loss所需参数

在计算loss的时候,实际上是y_pre和y_true之间的对比:
y_pre就是一幅 图像经过网络之后的输出,内部含有三个特征层的内容;其需要解码才能够在图上作画
y_true就是一个真实图像中,它的 每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
实际上y_pre和y_true内容的shape都是
(batch_size,13,13,3,85)
(batch_size,26,26,3,85)
(batch_size,52,52,3,85)

2、y_pre是什么

对于YoloV3的模型来说,网络 最后输出的内容就是三个特征层每个网格点对应的预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后, 每个网格点上三个先验框对应的位置、置信度及其种类。
对于 输出的y1、y2、y3而言,[…, : 2]指的是相对于每个网格点的偏移量,[…, 2: 4]指的是宽和高,[…, 4: 5]指的是该框的置信度,[…, 5: ]指的是每个种类的预测概率。
现在的y_pre还是没有解码的,解码了之后才是真实图像上的情况。

3、y_true是什么。

y_true就是一个真实图像中,它的 每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
在YoloV3中,其使用了一个专门的函数用于 处理读取进来的图片的框的真实情况。

def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):

其输入为:
true_boxes:shape为(m, T, 5)代表m张图T个框的x_min、y_min、x_max、y_max、class_id。
input_shape:输入的形状,此处为416、416
anchors:代表9个先验框的大小
num_classes:种类的数量。

其实对真实框的处理 是将真实框转化成图片中相对网格的xyhw,步骤如下:
1、取框的真实值,获取其框的中心及其宽高,除去input_shape变成比例的模式。
2、建立全为0的y_true,y_true是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
3、对每一张图片处理,将每一张图片中的真实框的wh和先验框的wh对比,计算IOU值,选取其中IOU最高的一个,得到其所属特征层及其网格点的位置,在对应的y_true中将内容进行保存。

for t, n in enumerate(best_anchor):
    for l in range(num_layers):
        if n in anchor_mask[l]:

            i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
            j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')

            k = anchor_mask[l].index(n)
            c = true_boxes[b,t, 4].astype('int32')

            y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
            y_true[l][b, j, i, k, 4] = 1
            y_true[l][b, j, i, k, 5+c] = 1

对于最后输出的y_true而言,只有每个图里每个框最对应的位置有数据,其它的地方都为0。
preprocess_true_boxes全部的代码如下:

def preprocess_true_boxes(self, true_boxes, input_shape, anchors, num_classes):
    assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'

    true_boxes  = np.array(true_boxes, dtype='float32')
    input_shape = np.array(input_shape, dtype='int32')

    num_layers  = len(self.anchors_mask)

    m           = true_boxes.shape[0]
    grid_shapes = [input_shape // {0:32, 1:16, 2:8}[l] for l in range(num_layers)]

    y_true = [np.zeros((m, grid_shapes[l][0], grid_shapes[l][1], len(self.anchors_mask[l]), 5 + num_classes),
                dtype='float32') for l in range(num_layers)]

    boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
    boxes_wh =  true_boxes[..., 2:4] - true_boxes[..., 0:2]

    true_boxes[..., 0:2] = boxes_xy / input_shape[::-1]
    true_boxes[..., 2:4] = boxes_wh / input_shape[::-1]

    anchors         = np.expand_dims(anchors, 0)
    anchor_maxes    = anchors / 2.

    anchor_mins     = -anchor_maxes

    valid_mask = boxes_wh[..., 0]>0

    for b in range(m):

        wh = boxes_wh[b, valid_mask[b]]
        if len(wh) == 0: continue

        wh          = np.expand_dims(wh, -2)
        box_maxes   = wh / 2.

        box_mins    = - box_maxes

        intersect_mins  = np.maximum(box_mins, anchor_mins)
        intersect_maxes = np.minimum(box_maxes, anchor_maxes)
        intersect_wh    = np.maximum(intersect_maxes - intersect_mins, 0.)
        intersect_area  = intersect_wh[..., 0] * intersect_wh[..., 1]

        box_area    = wh[..., 0] * wh[..., 1]
        anchor_area = anchors[..., 0] * anchors[..., 1]

        iou = intersect_area / (box_area + anchor_area - intersect_area)

        best_anchor = np.argmax(iou, axis=-1)

        for t, n in enumerate(best_anchor):

            for l in range(num_layers):
                if n in self.anchors_mask[l]:

                    i = np.floor(true_boxes[b,t,0] * grid_shapes[l][1]).astype('int32')
                    j = np.floor(true_boxes[b,t,1] * grid_shapes[l][0]).astype('int32')

                    k = self.anchors_mask[l].index(n)

                    c = true_boxes[b, t, 4].astype('int32')

                    y_true[l][b, j, i, k, 0:4] = true_boxes[b, t, 0:4]
                    y_true[l][b, j, i, k, 4] = 1
                    y_true[l][b, j, i, k, 5+c] = 1

    return y_true

4、loss的计算过程

在得到了y_pre和y_true后怎么对比呢?不是简单的减一下就可以的呢。
loss值需要对三个特征层进行处理,这里以最小的特征层为例。
1、利用y_true取出该特征层中真实存在目标的点的位置(m,13,13,3,1)及其对应的种类(m,13,13,3,80)。
2、将yolo_outputs的预测值输出进行处理,得到reshape后的预测值y_pre,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。还有解码后的xy,wh。
3、获取真实框编码后的值,后面用于计算loss,编码后的值其含义与y_pre相同,可用于计算loss。
4、对于每一幅图,计算其中所有真实框与预测框的IOU,取出每个网络点中IOU最大的先验框,如果这个最大的IOU都小于ignore_thresh,则保留,一般来说ignore_thresh取0.5,该步的目的是为了平衡负样本。
5、计算xy和wh上的loss,其计算的是实际上存在目标的,利用第三步真实框编码后的的结果和未处理的预测结果进行对比得到loss。
6、计算置信度的loss,其有两部分构成,第一部分是实际上存在目标的,预测结果中置信度的值与1对比;第二部分是实际上不存在目标的,在第四步中得到其最大IOU的值与0对比。
7、计算预测种类的loss,其计算的是实际上存在目标的,预测类与真实类的差距。

其实际上计算的总的loss是三个loss的和,这三个loss分别是:

  • 实际存在的框, 编码后的长宽与xy轴偏移量与预测值的差距
  • 实际存在的框, 预测结果中置信度的值与1对比;实际不存在的框,在上述步骤中,在第四步中得到其最大IOU的值与0对比
  • 实际存在的框, 种类预测结果与实际结果的对比

其实际代码如下,使用yolo_loss就可以获得loss值:

import tensorflow as tf
from tensorflow.keras import backend as K
from utils.utils_bbox import decode_anchors, get_grid_anchors

def box_iou(b1, b2):

    b1          = K.expand_dims(b1, -2)
    b1_xy       = b1[..., :2]
    b1_wh       = b1[..., 2:4]
    b1_wh_half  = b1_wh/2.

    b1_mins     = b1_xy - b1_wh_half
    b1_maxes    = b1_xy + b1_wh_half

    b2          = K.expand_dims(b2, 0)
    b2_xy       = b2[..., :2]
    b2_wh       = b2[..., 2:4]
    b2_wh_half  = b2_wh/2.

    b2_mins     = b2_xy - b2_wh_half
    b2_maxes    = b2_xy + b2_wh_half

    intersect_mins  = K.maximum(b1_mins, b2_mins)
    intersect_maxes = K.minimum(b1_maxes, b2_maxes)
    intersect_wh    = K.maximum(intersect_maxes - intersect_mins, 0.)
    intersect_area  = intersect_wh[..., 0] * intersect_wh[..., 1]
    b1_area         = b1_wh[..., 0] * b1_wh[..., 1]
    b2_area         = b2_wh[..., 0] * b2_wh[..., 1]
    iou             = intersect_area / (b1_area + b2_area - intersect_area)

    return iou

def yolo_loss(args, input_shape, anchors, anchors_mask, num_classes, ignore_thresh=.5, print_loss=False):
    num_layers = len(anchors_mask)

    y_true          = args[num_layers:]
    yolo_outputs    = args[:num_layers]

    input_shape = K.cast(input_shape, K.dtype(y_true[0]))
    batch_size  = K.cast(K.shape(yolo_outputs[0])[0], K.dtype(y_true[0]))

    m = K.shape(yolo_outputs[0])[0]

    grid_anchors    = []
    grid_whs        = []
    reshape_outputs = []
    reshape_y_true  = []
    for l in range(len(anchors_mask)):
        grid_anchor, grid_wh = get_grid_anchors(yolo_outputs[l], anchors[anchors_mask[l]])
        grid_anchors.append(grid_anchor)
        grid_whs.append(grid_wh)

        reshape_outputs.append(K.reshape(yolo_outputs[l], [batch_size, -1, num_classes + 5]))
        reshape_y_true.append(K.reshape(y_true[l], [batch_size, -1, num_classes + 5]))

    grid_anchors    = K.concatenate(grid_anchors, axis = 0)
    grid_whs        = K.concatenate(grid_whs, axis = 0)
    reshape_outputs = K.concatenate(reshape_outputs, axis = 1)
    reshape_y_true  = K.concatenate(reshape_y_true, axis = 1)

    object_mask      = reshape_y_true[..., 4:5]

    true_class_probs = reshape_y_true[..., 5:]

    box_xy, box_wh, _, _ = decode_anchors(reshape_outputs, grid_anchors, grid_whs, input_shape)

    pred_box = K.concatenate([box_xy, box_wh])

    ignore_mask         = tf.TensorArray(K.dtype(y_true[0]), size = 1, dynamic_size = True)
    object_mask_bool    = K.cast(object_mask, 'bool')

    def loop_body(b, ignore_mask):

        true_box = tf.boolean_mask(reshape_y_true[b, ..., 0:4], object_mask_bool[b, ..., 0])

        iou = box_iou(pred_box[b], true_box)

        best_iou = K.max(iou, axis=-1)

        ignore_mask = ignore_mask.write(b, K.cast(best_iou < ignore_thresh, K.dtype(true_box)))
        return b + 1, ignore_mask

    _, ignore_mask = tf.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])

    ignore_mask = ignore_mask.stack()

    ignore_mask = K.expand_dims(ignore_mask, -1)

    raw_true_xy = reshape_y_true[..., :2] * grid_whs - grid_anchors[..., :2]
    raw_true_wh = K.log(reshape_y_true[..., 2:4] / grid_anchors[..., 2:] * input_shape[::-1])

    raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh))

    box_loss_scale = 2 - reshape_y_true[...,2:3] * reshape_y_true[...,3:4]

    xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, reshape_outputs[...,0:2], from_logits=True)

    wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh - reshape_outputs[...,2:4])

    confidence_loss = object_mask * K.binary_crossentropy(object_mask, reshape_outputs[...,4:5], from_logits=True) + \
                (1 - object_mask) * K.binary_crossentropy(object_mask, reshape_outputs[...,4:5], from_logits=True) * ignore_mask

    class_loss      = object_mask * K.binary_crossentropy(true_class_probs, reshape_outputs[...,5:], from_logits=True)

    xy_loss         = K.sum(xy_loss)
    wh_loss         = K.sum(wh_loss)
    confidence_loss = K.sum(confidence_loss)
    class_loss      = K.sum(class_loss)

    num_pos = tf.maximum(K.sum(K.cast(object_mask, tf.float32)), 1)
    loss    = xy_loss + wh_loss + confidence_loss + class_loss
    if print_loss:
        loss = tf.Print(loss, [loss, xy_loss, wh_loss, confidence_loss, class_loss, tf.shape(ignore_mask)], summarize=100, message='loss: ')
    loss = loss / num_pos
    return loss

训练自己的YoloV3模型

首先前往Github下载对应的仓库,下载完后利用解压软件解压,之后用编程软件打开文件夹。
注意打开的根目录必须正确,否则相对目录不正确的情况下,代码将无法运行。

一定要注意打开后的根目录是文件存放的目录。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

; 一、数据集的准备

本文使用VOC格式进行训练,训练前需要自己制作好数据集,如果没有自己的数据集,可以通过Github连接下载VOC12+07的数据集尝试下。
训练前将标签文件放在VOCdevkit文件夹下的VOC2007文件夹下的Annotation中。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
训练前将图片文件放在VOCdevkit文件夹下的VOC2007文件夹下的JPEGImages中。
睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
此时数据集的摆放已经结束。

二、数据集的处理

在完成数据集的摆放之后,我们需要对数据集进行下一步的处理,目的是获得训练用的2007_train.txt以及2007_val.txt,需要用到根目录下的voc_annotation.py。

voc_annotation.py里面有一些参数需要设置。
分别是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次训练可以仅修改classes_path

'''
annotation_mode用于指定该文件运行时计算的内容
annotation_mode为0代表整个标签处理过程,包括获得VOCdevkit/VOC2007/ImageSets里面的txt以及训练用的2007_train.txt、2007_val.txt
annotation_mode为1代表获得VOCdevkit/VOC2007/ImageSets里面的txt
annotation_mode为2代表获得训练用的2007_train.txt、2007_val.txt
'''
annotation_mode     = 0
'''
必须要修改,用于生成2007_train.txt、2007_val.txt的目标信息
与训练和预测所用的classes_path一致即可
如果生成的2007_train.txt里面没有目标信息
那么就是因为classes没有设定正确
仅在annotation_mode为0和2的时候有效
'''
classes_path        = 'model_data/voc_classes.txt'
'''
trainval_percent用于指定(训练集+验证集)与测试集的比例,默认情况下 (训练集+验证集):测试集 = 9:1
train_percent用于指定(训练集+验证集)中训练集与验证集的比例,默认情况下 训练集:验证集 = 9:1
仅在annotation_mode为0和1的时候有效
'''
trainval_percent    = 0.9
train_percent       = 0.9
'''
指向VOC数据集所在的文件夹
默认指向根目录下的VOC数据集
'''
VOCdevkit_path  = 'VOCdevkit'

classes_path用于指向检测类别所对应的txt,以voc数据集为例,我们用的txt为:

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
训练自己的数据集时,可以自己建立一个cls_classes.txt,里面写自己所需要区分的类别。

三、开始网络训练

通过voc_annotation.py我们已经生成了2007_train.txt以及2007_val.txt,此时我们可以开始训练了。
训练的参数较多,大家可以在下载库后仔细看注释,其中最重要的部分依然是train.py里的classes_path。

classes_path用于指向检测类别所对应的txt,这个txt和voc_annotation.py里面的txt一样!训练自己的数据集必须要修改!

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
修改完classes_path后就可以运行train.py开始训练了,在训练多个epoch后,权值会生成在logs文件夹中。
其它参数的作用如下:
'''
是否使用eager模式训练
'''
eager = False
'''
训练前一定要修改classes_path,使其对应自己的数据集
'''
classes_path    = 'model_data/voc_classes.txt'
'''
anchors_path代表先验框对应的txt文件,一般不修改。
anchors_mask用于帮助代码找到对应的先验框,一般不修改。
'''
anchors_path    = 'model_data/yolo_anchors.txt'
anchors_mask    = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
'''
权值文件请看README,百度网盘下载
训练自己的数据集时提示维度不匹配正常,预测的东西都不一样了自然维度不匹配
预训练权重对于99%的情况都必须要用,不用的话权值太过随机,特征提取效果不明显
网络训练的结果也不会好,数据的预训练权重对不同数据集是通用的,因为特征是通用的
'''
model_path      = 'model_data/yolo_weight.h5'
'''
输入的shape大小,一定要是32的倍数
'''
input_shape     = [416, 416]
'''
训练分为两个阶段,分别是冻结阶段和解冻阶段
冻结阶段训练参数
此时模型的主干被冻结了,特征提取网络不发生改变
占用的显存较小,仅对网络进行微调
'''
Init_Epoch          = 0
Freeze_Epoch        = 50
Freeze_batch_size   = 8
Freeze_lr           = 1e-3
'''
解冻阶段训练参数
此时模型的主干不被冻结了,特征提取网络会发生改变
占用的显存较大,网络所有的参数都会发生改变
'''
UnFreeze_Epoch      = 100
Unfreeze_batch_size = 4
Unfreeze_lr         = 1e-4
'''
是否进行冻结训练,默认先冻结主干训练后解冻训练。
'''
Freeze_Train        = True
'''
用于设置是否使用多线程读取数据,0代表关闭多线程
开启后会加快数据读取速度,但是会占用更多内存
keras里开启多线程有些时候速度反而慢了许多
在IO为瓶颈的时候再开启多线程,即GPU运算速度远大于读取图片的速度。
'''
num_workers         = 0
'''
获得图片路径和标签
'''
train_annotation_path   = '2007_train.txt'
val_annotation_path     = '2007_val.txt'

四、训练结果预测

训练结果预测需要用到两个文件,分别是yolo.py和predict.py。
我们首先需要去yolo.py里面修改model_path以及classes_path,这两个参数必须要修改。

model_path指向训练好的权值文件,在logs文件夹里。
classes_path指向检测类别所对应的txt。

睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台
完成修改后就可以运行predict.py进行检测了。运行后输入图片路径即可检测。

Original: https://blog.csdn.net/weixin_44791964/article/details/119614577
Author: Bubbliiiing
Title: 睿智的目标检测51——Tensorflow2搭建yolo3目标检测平台

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/680808/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球