通用物体检测算法-YOLOv3详细解读

一、基本概念

1. 什么是目标检测

目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的类别和位置,是计算机视觉领域的核心问题之一。由于各类物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问题。

计算机视觉中关于图像识别有四大类任务:
(1)分类-Classification:解决“是什么?”的问题,即给定一张图片或一段视频判断里面包含什么类别的目标。
(2)定位-Location:解决“在哪里?”的问题,即定位出这个目标的的位置。
(3)检测-Detection:解决“在哪里?是什么?”的问题,即定位出这个目标的位置并且知道目标物是什么。

所以,目标检测是一个分类、回归问题的叠加。

2. 目标检测算法分类

基于深度学习的目标检测算法主要分为两类:Two stage和One stage。
1)Tow Stage先进行区域生成,该区域称之为region proposal(简称RPN,一个有可能包含待检物体的预选框),再通过卷积神经网络进行样本分类。
任务流程:特征提取 --> 生成RPN --> 分类/定位回归。
常见tow stage目标检测算法有:R-CNN、SPP-Net、Fast R-CNN、Faster R-CNN等。

2)One Stage
不用RPN,直接在网络中提取特征来预测物体分类和位置。
任务流程:特征提取–> 分类/定位回归。
常见的one stage目标检测算法有:OverFeat、YOLOv1、YOLOv2、YOLOv3、SSD和RetinaNet等。

二. YOLOv3目标检测算法

YOLO系列属于One Stage中的一种,作为一个小而美,快而准的目标检测网络,在图像检测领域上饱受赞誉,从yolov1->yolov3,也是在一直在不断进化,作为one-stage检测界的扛把子,只要做目标检测,没有理由不去了解YOLO! 本文以Yolov3结构为切入点,剖析Yolov3的网络结构。
通用物体检测算法-YOLOv3详细解读_第1张图片

1.YOLO网络部分

YOLO网络部分可以分成特征提取backbone主体(darknet_body),Neck脖子(make_last_layer, upsamples),和头部(YOLO_head)
Backbone:
backbone负责提取特征,输出feature map。
DarknetConv2D_BN_Leaky:就是普通的Conv2d->BN->LeakyReLU结构,也就是上面YOLOv3结构示意图中的最小单元DBL。

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

Resblock_body:输出 feature_map
这个是构建基于Yolov3 backbone的小轮子, 每用一次resblock_body() ,意味着feature map的scale缩小一倍

def resblock_body(x, num_filters, num_blocks):
    '''
    x:输入, num_filters:输出通道, num_blocks,重复块的次数
    '''
    # 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)#此处使用了stride=2降低分辨率(不用pooling)
    #重复构建残差网络,这个算基本的block
    for i in range(num_blocks):
        y = compose(
                DarknetConv2D_BN_Leaky(num_filters//2, (1,1)),#使用1x1的conv先将通道先降//2,
                DarknetConv2D_BN_Leaky(num_filters, (3,3)))(x)#使用3x3将通道后拉升回去
        x = Add()([x, y])#add
    return x

backbone–darknet_body:下面是Yolov3中darknet的网络结构代码:

def darknet_body(x):#darknet主干网络
    '''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

Neck
脖子部分功能:YOLO要把feature map中蕴含的信息转换为坐标,类别,这就需要把feature map的维度使用conv拉到指定的维度,结合anchor训练来输出关键性信息;同时为了提高回归位置精度,借鉴了fpn的思想,需要多个scale的feature map来提供最后的输出,因此还要用到一些upsample和concat。
通用物体检测算法-YOLOv3详细解读_第2张图片

head:
头部功能:得到了网络的输出,要和真实数据标注对接上计算loss,需要对数据的格式进行reshape,同时对原始grid_cell中的(x,y,w,h)做相应的激活,yolo_head主要干这些。
1.网络head输出:利用2层卷积操作输出我们想要尺寸的tensor(维度(NHW*(anchorcount*(class+4+1)))),也是网络原始输出;
2.anchor生成:利用设置的anchor(利用聚类算法,每个分支有3个共9组尺寸的anchor)生成整个特征图上所有的anchor,方便后续计算。
3.gt box网格的分配:gt box按照中心落入那个网格,那个网格负责的原则提前分配好,方便后续计算。
4.正负样本分配:将全部anchor根据和gt box的iou以及分配的网络,划分为正、负、忽略样本;
5.样本采样:为了平衡正负样本,按照一定规则(例如随机采样)选择部分anchor进行后续loss计算,yolov3全部采样;
6.gt box编码:将gt box编码为网络输出的相同形式,方便直接计算loss;
7.loss 计算:计算分类、confidence、矩形框位置和宽高的loss,并加权求和最终输出,供计算梯度和反向传播;


bbox_head=dict(
type='YOLOV3Head', # Yolo v3 head 类名
num_classes=80, # 预测类别
in_channels=[512, 256, 128], # heads输出tensor第一层卷积的输入channel
out_channels=[1024, 512, 256], # heads输出tensor第一层卷积的输出channel
anchor_generator=dict(
    type='YOLOAnchorGenerator', # Yolo的anchor生成器
    # anchors,供9个
    base_sizes=[[(116, 90), (156, 198), (373, 326)], [(30, 61), (62, 45), (59, 119)],
    [(10, 13), (16, 30), (33, 23)]],
    strides=[32, 16, 8]), # 输出特征图的stride
    bbox_coder=dict(type='YOLOBBoxCoder'), # bbox的编码器,负责gt box的编码和pred box的解码
featmap_strides=[32, 16, 8],  # 输出特征图的stride
loss_cls=dict(type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0,
reduction='sum'),# 类别loss
loss_conf=dict(type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0, reduction='sum'), # confidence loss
loss_xy=dict(type='CrossEntropyLoss', use_sigmoid=True, loss_weight=2.0, reduction='sum'), # box的位置loss
loss_wh=dict(type='MSELoss', loss_weight=2.0, reduction='sum'))) # box的宽高loss
# 正负样本分配类,负责所有anchor的正、负、忽略样本的分配
train_cfg = dict(
assigner=dict(type='GridAssigner', pos_iou_thr=0.5, neg_iou_thr=0.5, min_pos_iou=0))

下面分别来讲解head流程,这里假定batchSize为8,网络的输入为尺寸[8,3,320,320] 的tensor 。
通用物体检测算法-YOLOv3详细解读_第3张图片

anchor 生成
由于Yolo系列都是采用grid cell的方式划分样本位置,因此anchor只有宽高两个属性。预测box的位置(x,y)是相对于其对应grid cell偏移的,其大小是相对anchor的宽高。这点和Faster rcnn以及SSD等算法不一样。mmdetection为兼容两种做法,将anchor的生成统一到相同的形式上来,既利用AnchorGenerator生成特征图上所有的位置的anchor,这里的anchor是有位置属性的。Yolo 的anchor的类为YoloAnchorGenerator。该类主要完成两个任务:1,anchor的生成;2,gt box在grid cell的分配。

anchor生成
这里anchor的表达形式为左上点和右下点,既[x_0,y_0,x_1,y_1],核心代码如下:

base_anchor = torch.Tensor([
  x_center - 0.5 * w, y_center - 0.5 * h, x_center + 0.5 * w, y_center + 0.5 * h]

这里的x_center和y_center为base的grid cell的中心点坐标,即原图尺度的左上角第一个格子的中心坐标。例如在尺度为20X20的特征图上,其x和y方向的stride均为320/20=16,因此x_center和y_center为[stride_x/2,stride_y/2]=[8,8]。最终获取的一层输出的base_anchors 尺度为3X4,其中3为anchor个数。然后再通过grid_anchors()方法将base_anchors扩充到整个特征图上,为了后续计算方便,对特征图的宽高wh拉成一个维度。最终得到的anchor_list是长度为8(batchsize)的list,list中每一个元素是长度为3(输出层的个数)的list,内包含3个tensor,尺度分别为300X4(3个anchor X 特征图宽10 X 特征图高10,下同),1200X4,4800X4。
gt box 在grid cell中的分配
正如前文所说,Yolo系列按照grid cell来分配样本。gt box的中心点落入哪一个grid cell,哪一个grid cell负责预测该gt box。通过对gt box的分配,最终获取和anchor_list外两层同样尺度的数据,内部tensor长度为特征图宽X特征图高X anchor数目,值为1代表该物体属于该anchor预测(不是真的需要它来负责,下面还会根据iou再次筛选,可以理解为候选anchor)。代码如下:

feat_h, feat_w = featmap_size
# 获取gt的中心位置
gt_bboxes_cx = ((gt_bboxes[:, 0] + gt_bboxes[:, 2]) * 0.5).to(device)
gt_bboxes_cy = ((gt_bboxes[:, 1] + gt_bboxes[:, 3]) * 0.5).to(device)
# 将gt的中心位置映射到特征图尺寸
gt_bboxes_grid_x = torch.floor(gt_bboxes_cx / stride[0]).long()
gt_bboxes_grid_y = torch.floor(gt_bboxes_cy / stride[1]).long()
# 将w和h方向拉成一个维度
gt_bboxes_grid_idx = gt_bboxes_grid_y * feat_w + gt_bboxes_grid_x
# 记录gt所在的grid的mask,存在gt的位置设置为1
responsible_grid = torch.zeros(
  feat_h * feat_w, dtype=torch.uint8, device=device)
responsible_grid[gt_bboxes_grid_idx] = 1
# 将该mask推广到所有的anchor位置
responsible_grid = responsible_grid[:, None].expand(
  responsible_grid.size(0), num_base_anchors).contiguous().view(-1)
return responsible_grid

正负样本分配
该部分做的是确定正负样本,是在anchor维度上的。也就是确定所有的anchor哪些是正样本,哪些是负样本。划分为正样本的anchor意味着负责gt box的预测,训练的时候就会计算gt box的loss。而负样本表明该anchor没有负责任何物体,当然也需要计算loss,但是只计算confidence loss,因为没有目标,所以无法计算box loss 和类别loss。Yolo还有一个设置就是忽略样本,也就是anchor和gt box有较大的iou,但是又不负责预测它,就忽略掉,不计算任何loss。防止有错误的梯度更新到网络,也是为了提高网络的召回率。这里总结如下:

正样本:负责预测gt box的anchor。loss计算box loss(包括中心点+宽高)+confidence loss + 类别loss。
负样本:不负责预测gt box的anchor。loss只计算confidence loss。
忽略样本:和gt box的iou大于一定阈值,但又不负责该gt box的anchor,一般指中心点grid cell附近的其他grid cell 里的anchor。不计算任何loss。

样本采样
在目标检测中,为了保证正负样本平衡,一般采用了采样设置。但通常情况下, Yolov3 所有的样本都有用到,所以采用默认的采样器PseudoSampler,不做任何的采样操作。只是把anchor和gt box 选出来(按照GridAssigner中的信息),这里不再叙述。

gt box编码
分配好正负样本,需要计算loss。因此gt box要和预测的tensor统一到相同的表达上来。经过样本分配和采样操作,最终获取到配对的anchor和gt box,数目是完全相等的。因为为了便于计算,这里将gt box 复制到和样本的anchor相同的数目。如下所示,gt box编码利用self.bbox_coder.encode进行。

# gt box编码
target_map[sampling_result.pos_inds, :4] = self.bbox_coder.encode(
  sampling_result.pos_bboxes, sampling_result.pos_gt_bboxes,
  anchor_strides[sampling_result.pos_inds])
# target的confidence 全部设置为1,v2中采用的是iou,值得注意
target_map[sampling_result.pos_inds, 4] = 1

前面提到过,Yolo根据grid cell分配box的位置,根据anchor的大小预测box宽高,因此mmdetection将gt box或预测box编码和解码的操作抽象出一个类。在yolo_bbox_coder.py中类YOLOBBoxCoder,提供两个方法:encode()和decode(),分别进行gt box的编码和预测box的解码。解码部分是将网络直接预测的值根据anchor还原到gt原图的表达形式,不再叙述。下面是编码方法:

def encode(self, bboxes, gt_bboxes, stride)
    # 作用是将gt box利用grid cell和anchor编码成网络输出的形式,
    # 为了方便和网络的输出直接计算loss。其中bboxes是指anchor,获取gt的中心点和宽高
    x_center_gt = (gt_bboxes[..., 0] + gt_bboxes[..., 2]) * 0.5
    y_center_gt = (gt_bboxes[..., 1] + gt_bboxes[..., 3]) * 0.5
    w_gt = gt_bboxes[..., 2] - gt_bboxes[..., 0]
    h_gt = gt_bboxes[..., 3] - gt_bboxes[..., 1]
    # 获取anchor的中心点和宽高
    x_center = (bboxes[..., 0] + bboxes[..., 2]) * 0.5
    y_center = (bboxes[..., 1] + bboxes[..., 3]) * 0.5
    w = bboxes[..., 2] - bboxes[..., 0]
    h = bboxes[..., 3] - bboxes[..., 1]
    # 计算target
    w_target = torch.log((w_gt / w).clamp(min=self.eps))
    h_target = torch.log((h_gt / h).clamp(min=self.eps))
    
# 注意加上0.5的作用是,anchor保存的是相对grid cell中心点box,而网络预测是相对于grid cell的左上角,     # 因此在此上加0.5(做过归一化) 就可以解析到左上角
    x_center_target = ((x_center_gt - x_center) / stride + 0.5).clamp(
      self.eps, 1 - self.eps)
    y_center_target = ((y_center_gt - y_center) / stride + 0.5).clamp(
      self.eps, 1 - self.eps)
    encoded_bboxes = torch.stack(
      [x_center_target, y_center_target, w_target, h_target], dim=-1)
    return encoded_bboxes

loss 计算
至此,所有的anchor全部计算出来并完成了分配,可以直接进行loss的计算了。经过前面的转化,这里遍历所有输出分支(3个)进行loss计算,如下:

# 在样本上计算分类
loss_cls = self.loss_cls(pred_label, target_label, weight=pos_mask)
# 在正+负样本上计算confidence
loss_conf = self.loss_conf(pred_conf, target_conf, weight=pos_and_neg_mask)
# 在正样本上计算中心点损失和宽高损失
loss_xy = self.loss_xy(pred_xy, target_xy, weight=pos_mask)
loss_wh = self.loss_wh(pred_wh, target_wh, weight=pos_mask)

最后将全部loss按照一定的比例加起来构成最终的损失,可以愉快地进行求梯度和反向传播了。

你可能感兴趣的:(python,算法,机器学习,深度学习)