Yolo v3目标检测网络详解

Yolo介绍

Yolo(You only look once)是经典的单阶段目标检测方法, 它于2016年提出第一版Yolov1,至今仍有许多基于它的改进模型。本文主要介绍的Yolov3就是其中之一。
首先来介绍Yolo v1、v2(Yolo9000)以及Yolov3之间有什么区别及改进之处。
Yolov1首先将目标检测作为一个回归问题来解决,使用单个神经网络直接从整个图片中预测边界框以及类别概率,它速度很快,可以做到实时目标检测。
Yolo v2相比yolo v1更快,而且更准。它有以下几点改进:
1、使用了BatchNorm,让网络更容易拟合。
2、在预训练的ImageNet模型上,使用更高分辨率图片对模型进行fine-tune,得到了更好的分类器。
3、使用了anchor,去除了yolo v1中的全连接层,此处借鉴了faster rcnn的anchor策略,全部改为卷积层来预测边界框。
4、使用了维度聚类的方法(以iou为距离度量,对先验框进行kmeans聚类),获得了更好的先验框,让网络得到了更好的预测结果。
5、多尺度训练,使用了不同尺寸的图片进行训练,使网络可以适应不同分辨率大小的数据。
6、直接预测坐标,直接预测物体中心相对于所处网格的坐标。
7、跳过连接,融合不同尺度的特征。

Yolo v3相比于yolo v2精度更高,速度稍有下降。它主要在以下几个方面进行了改进:
1、特征提取网络采用了残差结构,并且层数更多。
2、yolo v3在3个尺度上进行检测,依次检测大、中、小物体。

Yolo v3网络结构

首先介绍Yolov3中的backbone网络,如下图所示,左边为Yolov2使用的backbone(Darknet19),右边为Yolov3使用的backbone(Darknet53),Darknet53为全卷积结构,它去掉了所有的MaxPooling层,并且增加了更多的卷积层。它共包含了23个残差块,经过了5次下采样,最后backbone的输出大小为网络输入的1/32。由于网络的加深,Yolov3的速度比Yolov2稍慢。

Snipaste_2020-02-18_17-54-58.png

Yolo v3的网络结构如下图所示,Conv2D block包含了5个卷积层,整个网络结构相对比较简单,比两阶段检测方法,比如faster rcnn要容易许多。

Yolo3.png

Yolo v3实现过程

  1. 输入图片通过backbone得到3个尺度的特征图(从上往下:feat1 -> (256 * 52 * 52), feat2 -> (512 * 26 * 26), feat3 -> (1024 * 13 * 13)),分别在3种尺度上进行检测。
  2. 3个特征图经过5层卷积(Conv2D Block)后,分别进入不同的分支,一条分支进行卷积+上采样,得到的特征图与上层的特征图进行通道合并(Concat),另一条分支通过两层卷积直接输出预测结果。
  3. 最后一个卷积层为1 * 1卷积,卷积核尺寸为(B * (5 + C)) * 1 * 1,B表示一个网格可以预测边界框的数目,C代表C个类别概率,5表示了4个坐标值(tx,ty,tw, th)和1个物体置信度。对于coco数据集,C=80,B=3。最终3个尺度的检测结果的尺寸分别是255 * 52 * 52、255 * 26 * 26和255 * 52 * 52。

下图展示了在feat3特征图上的检测结果,特征图上的一个像素对应原图中的一个网格,每个尺度定义了3种anchor,即每个网格会有3个预测框,每个预测框具有(5 + C)个属性。网络在3个尺度上检测,所以整个网络共检测13 * 13 * 3 + 26 * 26 * 3 + 52 * 52 * 3 = 10647个边界框。
0_3A8U0Hm5IKmRa6hu.png

Yolov3误差

Yolov3的误差包括了置信度误差、分类误差和定位误差。前面提到每个预测框具有(5 + C)个属性,5表示了4个坐标值(tx,ty,tw,th)和1个物体置信度,这里的4个坐标是相对于物体中心所处网格的左上角而言的,物体置信度表示该预测框包含物体的概率,包含物体则置信度为1,否则为0。C表示C个类别概率。

如下图表示了Yolov1网络(没找到Yolov3的误差的公式)的误差组成,Yolov3把后面三项都改成了二分类交叉熵误差了。

绿色框和黄色框表示置信度误差(conf_loss),绿色框表示了在第i个网格中的第j个预测框包含物体时的conf_loss,黄色框表示在第i个网格中的第j个预测框不包含物体时的conf_loss。

红色框表示定位误差(loc_loss),只有预测框包含物体才计算定位误差,定位误差中。

紫色框表示了分类误差(cls_loss),Pi(c)表示第i个网格中属于第c个类别的条件概率。只有物体出现在第i个网格内,该网格才负责检测这个物体,才计算分类误差。

image.png

置信度误差和分类误差都使用二分类损失误差(BCE)。至于分类误差并未使用多分类交叉熵误差,原文是这样解释的,使用单独的逻辑分类器有助于网络在开放数据集上的表现,这些数据集内通常包含重叠的标签(比如人和男人),使用softmax就意味着每个预测框必须是确定的某一个类别,而使用多标签的方法可以更好对数据建模。定位误差使用平方根损失误差(MSE)。

另外,为了缓解背景框和前景框的不平衡问题(在所有的预测框中,前景框只有极少一部分),增加了额外两个参数λcoor和λnoobj,在Yolov1中λcoor=5,λnoobj=0.5,提高前景框定位误差的权重,降低背景框的权重。

接着,来介绍一下预测坐标和预测框之间的关系。预测坐标只是相对于网格的坐标,我们需要将预测坐标转换为相对于原图的坐标。下图展示了预测坐标与预测框之间的对应关系。

yolo-regression-1.png

参数说明
$\sigma$: sigmoid函数
$P_h$: 预定义的anchor高度
$P_h$: 预定义的anchor宽度
$grid\_x$: 物体中心所处网格左上角x坐标
$grid\_y$: 物体中心所处网格左上角y坐标
网络预测坐标为:$t_x,t_y,t_w,t_h$
最终预测框的坐标为:
$$\begin{cases}x = \sigma(t_x) + grid\_x \\y = \sigma(t_y) + grid\_y\\ w = e^{t_w} * P_w \\h = e^{t_h} * P_h\end{cases}$$

Yolov3主要代码

网络构建

网络搭建使用yolo官方的配置文件yolov3.cfg进行构建,以下结合cfg配置文件对网络中各个网络层进行简要说明:

#-----------------------------------------#
#---------------网络的超参数---------------#
#-----------------------------------------#
[net]
# Testing
#batch=1
#subdivisions=1
# Training
batch=16
subdivisions=1
# 网络输入图片的宽度
width=416
# 网络输入图片的高度
height=416
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
learning_rate=0.001
burn_in=1000
max_batches = 500200
policy=steps
steps=400000,450000
scales=.1,.1

#-----------------------------------------#
#-------------------卷积层-----------------#
#-----------------------------------------#
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky

#-----------------------------------------#
#-------------------捷径层-----------------#
#-----------------------------------------#
# 残差连接
[shortcut]
# 指示前一层输出与哪一层进行残差连接
# -3从后往前第3层
from=-3
activation=linear


#-----------------------------------------#
#-------------------捷径层-----------------#
#-----------------------------------------#
# 跳跃连接
[route]
# 指示前一层输出与哪一层进行跳跃连接(concat)
# -3从后往前第3层
layers = -4

#-----------------------------------------#
#------------------上采样层----------------#
#-----------------------------------------#
# 上采样
[upsample]  
stride=2

#-----------------------------------------#
#-------------------yolo层-----------------#
#-----------------------------------------#
[yolo]
# 指示yolo层采用anchors中的哪些anchor
mask = 6,7,8
# 表示网络共有9种预定义anchor大小
anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
# 数据集类别数
classes=80
# 预定义anchor数目
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1

Yolo层

class YoloLayer(nn.Module):

    """
    Params
        anchors(list): 预定义的anchor,列表中的每一个元素对应一个anchor大小
        num_class(integer): 类别数目
        img_dim(integer): 输入图片尺寸(长宽一致)
    """
    def __init__(self, anchors, num_class, img_dim):
        super(YoloLayer, self).__init__()
        self.anchors = anchors
        self.num_anchors = len(anchors)
        self.num_class = num_class
        self.img_dim = img_dim


        # 每个anchors具有的属性向量长度为 ({x, y, h, w, Pobj} + 类别数), 
        self.bbox_attrs = 5 + self.num_class

    def forward(self, x, calc_loss=True):
        """
        Params:
            x(tensor): shape -> (batch_size, 5 + class_num, img_dim / x.shape[2], img_dim / x.shape[2])
        
        Return
            output(tensor): shape -> (batch_size, img_dim *img_dim / (x.shape[2]*x.shape[2]), 5 + class_num)
        """
        
        # 预定义anchor数目
        num_anchors = self.num_anchors

        batch_size = x.shape[0]
        
        # 特征图大小
        size = x.shape[2]
        
        # 计算步幅,即缩放倍数
        stride = self.img_dim / size

        prediction = x.view(batch_size, num_anchors, self.bbox_attrs, size, size).permute(0, 1, 3, 4, 2).contiguous()


        # 确保坐标的偏移量在0-1范围内
        pred_conf = t.sigmoid(prediction[..., 4])
        pred_cls = t.sigmoid(prediction[..., 5:])

        # 构建网格,根据预定义anchor尺寸,在每个网格上生成num_anchors个anchor
        grid_x, grid_y = t.meshgrid(t.arange(size, dtype=t.float32), t.arange(size, dtype=t.float32))
        grid_x, grid_y = grid_x.view(1, 1, size, size), grid_y.view(1, 1, size, size)

        # 将对应到原图的anchor大小缩放到对应到特征图的尺度
        scaled_anchors = t.tensor([[anchor_w / stride, anchor_h / stride] \
            for anchor_w, anchor_h in self.anchors], dtype=t.float32)

        anchor_w = scaled_anchors[:, 0].view(1, num_anchors, 1, 1)
        anchor_h = scaled_anchors[:, 1].view(1, num_anchors, 1, 1)
        pred_boxes = t.FloatTensor(prediction[..., :4].shape)
        
        # 基于预测坐标得到预测框
        pred_boxes[..., 0] = grid_x + t.sigmoid(prediction[..., 0])
        pred_boxes[..., 1] = grid_y + t.sigmoid(prediction[..., 1])
        pred_boxes[..., 2] = t.exp(prediction[..., 2]) * anchor_w
        pred_boxes[..., 3] = t.exp(prediction[..., 3]) * anchor_h

        # 合并预测框、置信度以及类别概率
        output = t.cat(
            (pred_boxes.view(batch_size, -1, 4) * stride,
            pred_conf.view(batch_size, -1, 1),
            pred_cls.view(batch_size, -1, self.num_class)),
        -1)
        if calc_loss:
            return pred_boxes, pred_cls, pred_conf, prediction[..., :4], output
         return output

网络误差

build_target是误差计算中最重要的部分,它的功能是匹配,即匹配真实框和负责预测该真实框的预测框。它的主要过程如下:

a. 首先将归一化的真实框转换到特征图尺寸上的真实框。
b. 计算预定义anchor和真实框的iou,获取与每个真实框的最大iou及其索引,即可得到每个真实框需要使用哪个尺寸的anchor来匹配。
c. 根据真实框所处网格位置以及每个真实框匹配的anchor,得到每个真实框匹配的预测框。
d. 根据真实框的坐标,得到真实框相对于物体所处网格的位置, 以便于与预测坐标计算误差。

代码如下:

def build_targets(pred_boxes, pred_cls, target, anchors, ignore_thres, eps=1e-8):
    """

    Params:
        pred_boxes: 预测框,shape --> `(batch_size, anchors_num, grid_size, grid_size, 4)`
        pred_cls: , 预测的类别概率,shape --> `(batch_size, anchors_num, grid_size, grid_size, num_class)`
        target: 真实地面框,每一个元素包含了对应的batch、类别以及边界框坐标,shape --> `(batch_size, 6)`
        anchors: 缩放后的预定义anchor的大小,shape --> `(3, 2)`
        ignore_thres: 前景阈值

    Return:
        iou_scores: 预测框与真实框的iou,shape --> `(batch_size, anchors_num, grid_size, grid_size)`
        class_mask: 预测的类别掩膜,shape --> `(batch_size, anchors_num, grid_size, grid_size)`
        obj_mask: shape --> `(batch_size, anchors_num, grid_size, grid_size)`
        noobj_mask: shape --> `(batch_size, anchors_num, grid_size, grid_size)` 
        txy: 变换后真实框的坐标,shape --> `(batch_size, anchors_num, grid_size, grid_size, 2)`
        twh: 变换后真实框的长宽,shape --> `(batch_size, anchors_num, grid_size, grid_size, 2)`
        tcls: 真实的分类结果, hot-encode形式,shape --> `(batch_size, anchors_num, grid_size, grid_size num_class)`, 
        tconf: shape --> `(batch_size, anchors_num, grid_size, grid_size)` 

    """
    batch_size = pred_boxes.shape[0]
    num_anchors = pred_boxes.shape[1]
    num_class = pred_cls.shape[-1]
    size = pred_boxes.shape[2]


    # output tensors
    # 初始化
    # λobj, anchor包含物体, 即为1,默认为0
    obj_mask = t.ByteTensor(batch_size, num_anchors, size, size).fill_(0)
    # λnoobj, anchor不包含物体, 则为1,默认为1
    noobj_mask = t.ByteTensor(batch_size, num_anchors, size, size).fill_(1)
    # 类别掩膜,类别预测正确即为1,默认全为0,
    class_mask = t.FloatTensor(batch_size, num_anchors, size, size).fill_(0),
    # 预测框与真实框的iou得分
    iou_score = t.FloatTensor(batch_size, num_anchors, size, size).fill_(0)

    # 真实框相对于网格的位置
    txy = t.FloatTensor(batch_size, num_anchors, size, size, 2).fill_(0)
    twh = t.FloatTensor(batch_size, num_anchors, size, size, 2).fill_(0)

    tcls = t.FloatTensor(batch_size, num_anchors, size, size, num_class).fill_(0)


    # target  --> (target_num, 6), 归一化的坐标值
    # 将归一化的真实框缩放到特征图尺寸上的真实框
    target_boxes = target[:, 2:] * size
    
    # 真实框的坐标
    xy = target_boxes[:, :2]
    
    # 真实框的尺寸
    wh = target_boxes[:, 2:]
    
    # 计算预定义anchor和真实框的iou,shape -> (len(anchors), len(wh))
    # Note: 这里只需要确定哪个预定义anchor的尺寸和真实框的尺寸最接近, 最后用尺寸最接近的anchor来对真实框进行回归, 因此只需要宽高即可
    iou = iou_wh(anchors, wh)
    
    # 根据iou,得到与每个真实框最大的iou及其索引
    best_iou, best_idx = iou.max(0)

    # 获取真实框对应的batch和类别标签
    b, target_label = target[:, :2].long().t()

    # 获取真实框所处网格的位置,即匹配每个真实框到对应的网格,得到每个真实框需要使用哪个网格进行预测
    gi, gj = xy.long().t()
    
    # 预测框中包含物体的mask
    obj_mask[b, best_idx, gj, gi] = 1
    noobj_mask[b, best_idx, gj, gi] = 0

    for i, anchor_iou in enumerate(iou.t()):
        noobj_mask[b[i], anchor_iou > ignore_thres, gj[i], gi[i]] = 0

    # 根据真实框的坐标和尺寸, 得到真实框相对于所处网格的位置, 以便于与预测坐标计算误差
    txy[b, best_idx, gj, gi] = xy - xy.floor()

    twh[b, best_idx, gj, gi] = t.log(wh / (eps + anchors[best_idx][:, :2]))

    # 将真实框的标签转换为one-hot编码形式
    tcls[b, best_idx, gj, gi, target_label] = 1

    class_mask[b, best_idx, gj, gi] = (pred_cls[b, best_idx, gj, gi].argmax(-1) == target_label).float()
    
    # 计算真实框相匹配的预测框和真实框之间的iou得分
    iou_score[b, best_idx, gj, gi] = bbox_iou(pred_boxes[b, best_idx, gj, gi], target_boxes)
    
    # 真实框的置信度
    tconf = obj_mask.float()

    return iou_score, class_mask, obj_mask, noobj_mask, txy, twh, tcls, tconf

计算误差,根据匹配好的真实框和预测框计算误差。代码如下

def calc_loss(prediction_coor, pred_conf, pred_cls, obj_mask, noobj_mask, txy, twh, tcls, tconf):
    """
    annotate
        size: 特征图大小
    Params
        prediction_coor: 网络预测坐标, shape -> (batch_size, num_anchors, size, size, 4)
        pred_conf: 预测的置信度, shape -> (batch_size, num_anchors, size, size)
        pred_cls: 预测的类别概率, shape -> (batch_size, num_anchors, size, size, class_num)
        obj_mask: 物体掩膜, 预测框包含物体即为1, shape -> (batch_size, num_anchors, size, size)
        noobj_mask: 非物体掩膜, 预测框不包含物体即为1, shape -> (batch_size, num_anchors, size, size)
        txy: 真实框中心坐标, shape -> (batch_size, num_anchors, size, size, 2)
        twh: 真实框大小, shape -> (batch_size, num_anchors, size, size, 2)
        tcls: 真实框类别one-hot编码, shape -> (batch_size, num_anchors, size, size, class_num)
        tconf: 真实框的置信度, shape -> (batch_size, num_anchors, size, size)
        
    Return
        total_loss(float): 总误差
    
    """
    
     xy = t.sigmoid(prediction_coor[..., :2])
     wh = prediction_coor[..., 2:4]
     
     # 定位误差
     # Note: 计算loc_loss之前,需要把坐标转换到所处网格的相对坐标和尺寸
     loss_xy = self.mse_loss(xy[obj_mask], txy[obj_mask])
     loss_wh = self.mse_loss(wh[obj_mask], twh[obj_mask])

     # 置信度误差,只能包含0和1
     loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
     loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
     
     # 给与包含物体的置信度误差和不包含物体的置信度误差不同的权重
     loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj

     # 分类误差
     loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask])
     
     # 总误差等于所有误差的总和
     total_loss = loss_xy + loss_wh + loss_conf + loss_cls

    return total_loss

Yolov3总结

相比于Yolov2,Yolov3加深了网络,并且采用了3种尺度进行检测,使用了更多的anchor,速度明显更慢(推理时间为29ms),但精度却提升了不少(mAP-50达到了55.3),并且对于小目标的检测,效果更好。

other triks

Anchor的选择

Yolov3共使用了9种尺寸的anchor,那么9种尺寸是如何选择的呢。yolov3使用了维度聚类策略,对coco数据集中的所有真实框进行kmeans聚类,最终产生9个镞,得到9个anchor尺寸。
聚类距离度量采用如下的度量方式:

d(box; centroid) = 1 - IOU(box; centroid)

Reference

https://arxiv.org/abs/1506.02640

https://arxiv.org/abs/1612.08242

https://arxiv.org/abs/1804.02767

https://github.com/eriklinder...

https://blog.csdn.net/weixin_...

https://towardsdatascience.co...

https://towardsdatascience.co...

你可能感兴趣的:(目标检测,python,pytorch)