万字长文解析 YOLOv1-v5 模型(上)

本文涉及的产品
交互式建模 PAI-DSW,每月250计算时 3个月
模型在线服务 PAI-EAS,A10/V100等 500元 1个月
模型训练 PAI-DLC,100CU*H 3个月
简介: 万字长文解析 YOLOv1-v5 模型

一,YOLOv1

YOLOv1 出自 2016 CVPR 论文 You Only Look Once:Unified, Real-Time Object Detection.

YOLO 系列算法的核心思想是将输入的图像经过 backbone 提取特征后,将得到特征图划分为 S x S 的网格,物体的中心落在哪一个网格内,这个网格就负责预测该物体的置信度、类别以及坐标位置

Abstract

作者提出了一种新的目标检测方法 YOLO,之前的目标检测工作都是重新利用分类器来执行检测。作者的神经网络模型是端到端的检测,一次运行即可同时得到所有目标的边界框和类别概率。

YOLO 架构的速度是非常快的,base 版本实时帧率为 45 帧,smaller 版本能达到每秒 155 帧,性能由于 DPMR-CNN 等检测方法。

1. Introduction

之前的目标检测器是重用分类器来执行检测,为了检测目标,这些系统在图像上不断遍历一个框,并利用分类器去判断这个框是不是目标。像可变形部件模型(DPM)使用互动窗口方法,其分类器在整个图像的均匀间隔的位置上运行。

作者将目标检测看作是单一的回归问题,直接从图像像素得到边界框坐标和类别概率

YOLO 检测系统如图 1 所示。单个检测卷积网络可以同时预测多个目标的边界框和类别概率。YOLO 和传统的目标检测方法相比有诸多优点。

网络异常,图片无法展示
|

首先,YOLO 速度非常快,我们将检测视为回归问题,所以检测流程也简单。其次,YOLO 在进行预测时,会对图像进行全面地推理。第三,YOLO 模型具有泛化能力,其比 DPMR-CNN 更好。最后,虽然 YOLO 模型在精度上依然落后于最先进(state-of-the-art)的检测系统,但是其速度更快。

2. Unified Detectron

YOLO 系统将输入图像划分成 S×SS\times SS×S 的网格(grid),然后让每个gird 负责检测那些中心点落在 grid 内的目标。

检测任务:每个网络都会预测 BBB 个边界框及边界框的置信度分数,所谓置信度分数其实包含两个方面:一个是边界框含有目标的可能性,二是边界框的准确度。前者记为 Pr(Object)Pr(Object)Pr(Object),当边界框包含目标时,Pr(Object)Pr(Object)Pr(Object) 值为 1,否则为 0;后者记为 IOUpredtruthIOU_{pred}^{truth}IOUpredtruth,即预测框与真实框的 IOU。因此形式上,我们将置信度定义为 Pr(Object)∗IOUpredtruthPr(Object)*IOU_{pred}^{truth}Pr(Object)IOUpredtruth。如果 grid 不存在目标,则置信度分数置为 0,否则,置信度分数等于预测框和真实框之间的交集(IoU)。

每个边界框(bounding box)包含 5 个预测变量:xxxyyywwwhhhconfidence(x,y)(x,y)(x,y) 坐标不是边界框中心的实际坐标,而是相对于网格单元左上角坐标的偏移(需要看代码才能懂,论文只描述了出“相对”的概念)。而边界框的宽度和高度是相对于整个图片的宽与高的比例,因此理论上以上 4 预测量都应该在 [0,1][0,1][0,1] 范围之内。最后,置信度预测表示预测框与实际边界框之间的 IOU

值得注意的是,中心坐标的预测值 (x,y)(x,y)(x,y) 是相对于每个单元格左上角坐标点的偏移值,偏移量 = 目标位置 - grid的位置。

网络异常,图片无法展示
|

分类任务:每个网格单元(grid)还会预测 CCC 个类别的概率 Pr(Classi)∣Object)Pr(Class_i)|Object)Pr(Classi)Object)grid 包含目标时才会预测 PrPrPr,且只预测一组类别概率,而不管边界框 BBB 的数量是多少。

在推理时,我们乘以条件概率和单个 box 的置信度。

Pr(Classi)∣Object)∗Pr(Object)∗IOUpredtruth=Pr(Classi)∗IOUpredtruthPr(Class_i)|Object)*Pr(Object)*IOU_{pred}^{truth} = Pr(Class_i)*IOU_{pred}^{truth}Pr(Classi)Object)Pr(Object)IOUpredtruth=Pr(Classi)IOUpredtruth

它为我们提供了每个框特定类别的置信度分数。这些分数编码了该类出现在框中的概率以及预测框拟合目标的程度。

Pscal VOC 数据集上评测 YOLO 模型时,我们设置 S=7S=7S=7, B=2B=2B=2(即每个 grid 会生成 2 个边界框)。Pscal VOC 数据集有 20 个类别,所以 C=20C=20C=20。所以,模型最后预测的张量维度是 7×7×(20+5∗2)=14707 \times 7\times (20+5*2) = 14707×7×(20+52)=1470

网络异常,图片无法展示
|

总结YOLO 系统将检测建模为回归问题。它将图像分成 S×SS \times SS×Sgird,每个 grid 都会预测 BBB 个边界框,同时也包含 CCC 个类别的概率,这些预测对应的就是 S×S×(C+5∗B)S \times S \times (C + 5*B)S×S×(C+5B)

这里其实就是在描述 YOLOv1 检测头如何设计:回归网络的设计 + 训练集标签如何构建(即 yoloDataset 类的构建),下面给出一份针对 voc 数据集编码为 yolo 模型的输入标签数据的函数,读懂了这个代码,就能理解前面部分的描述。

代码来源这里

def encoder(self, boxes, labels):
    '''
    boxes (tensor) [[x1,y1,x2,y2],[]] 目标的边界框坐标信息
    labels (tensor) [...] 目标的类别信息
    return 7x7x30
    '''
    grid_num = 7 # 论文中设为7
    target = torch.zeros((grid_num, grid_num, 30))  # 和模型输出张量维尺寸一样都是 14*14*30
    cell_size = 1./grid_num  # 之前已经把目标框的坐标进行了归一化(这里与原论文有区别),故这里用1.作为除数
    # 计算目标框中心点坐标和宽高
    wh = boxes[:, 2:]-boxes[:, :2]  
    cxcy = (boxes[:, 2:]+boxes[:, :2])/2  
    # 1,遍历各个目标框;
    for i in range(cxcy.size()[0]):    # 对应于数据集中的每个框 这里cxcy.size()[0] == num_samples
        # 2,计算第 i 个目标中心点落在哪个 `grid` 上,`target` 相应位置的两个框的置信度值设为 `1`,同时对应类别值也置为 `1`;
        cxcy_sample = cxcy[i]  
        ij = (cxcy_sample/cell_size).ceil()-1 # ij 是一个list, 表示目标中心点cxcy在归一化后的图片中所处的x y 方向的第几个网格
        # [0,1,2,3,4,5,6,7,8,9, 10-19] 对应索引 
        # [x,y,w,h,c,x,y,w,h,c, 20 个类别的 one-hot编码] 与原论文输出张量维度各个索引对应目标有所区别
        target[int(ij[1]), int(ij[0]), 4] = 1  # 第一个框的置信度
        target[int(ij[1]), int(ij[0]), 9] = 1  # 第二个框的置信度
        target[int(ij[1]), int(ij[0]), int(labels[i])+9] = 1 # 第 int(labels[i])+9 个类别为 1
        # 3,计算目标中心所在 `grid`(网格)的左上角相对坐标:`ij*cell_size`,然后目标中心坐标相对于子网格左上角的偏移比例 `delta_xy`;
        xy = ij*cell_size  
        delta_xy = (cxcy_sample -xy)/cell_size  
        # 4,最后将 `target` 对应网格位置的 (x, y, w, h) 分别赋相应 `wh`、`delta_xy` 值。
        target[int(ij[1]), int(ij[0]), 2:4] = wh[i]  # 范围为(0,1)
        target[int(ij[1]), int(ij[0]), :2] = delta_xy
        target[int(ij[1]), int(ij[0]), 7:9] = wh[i]
        target[int(ij[1]), int(ij[0]), 5:7] = delta_xy
    return target
复制代码

代码分析,一张图片对应的标签张量 target 的维度是 7×7×307 \times 7 \times 307×7×30。然后分别对各个目标框的 boxes: (x1,y1,x2,y2)(x1,y1,x2,y2)(x1,y1,x2,y2)labels(0,0,...,1,0)(one-hot 编码的目标类别信息)进行处理,符合检测系统要求的输入形式。算法步骤如下:

  1. 计算目标框中心点坐标和宽高,并遍历各个目标框;
  2. 计算目标中心点落在哪个 grid 上,target 相应位置的两个框的置信度值设为 1,同时对应类别值也置为 1
  3. 计算目标中心所在 grid(网格)的左上角相对坐标:ij*cell_size,然后目标中心坐标相对于子网格左上角的偏移比例 delta_xy
  4. 最后将 target 对应网格位置的 (x,y,w,h)(x, y, w, h)(x,y,w,h) 分别赋相应 whdelta_xy 值。

2.1. Network Design

YOLO 模型使用卷积神经网络来实现,卷积层负责从图像中提取特征,全连接层预测输出类别概率和坐标。

YOLO 的网络架构受 GooLeNet 图像分类模型的启发。网络有 24 个卷积层,最后面是 2 个全连接层。整个网络的卷积只有 1×11 \times 11×13×33 \times 33×3 卷积层,其中 1×11 \times 11×1 卷积负责降维 ,而不是 GoogLeNetInception 模块。

网络异常,图片无法展示
|
图3:网络架构。作者在 ImageNet 分类任务上以一半的分辨率(输入图像大小 224×224224\times 224 2 2 4 × 2 2 4)训练卷积层,但预测时分辨率加倍。

Fast YOLO 版本使用了更少的卷积,其他所有训练参数及测试参数都和 base YOLO 版本是一样的。

网络的最终输出是 7×7×307\times 7\times 307×7×30 的张量。这个张量所代表的具体含义如下图所示。对于每一个单元格,前 20 个元素是类别概率值,然后 2 个元素是边界框置信度,两者相乘可以得到类别置信度,最后 8 个元素是边界框的 (x,y,w,h)(x,y,w,h)(x,y,w,h) 。之所以把置信度 ccc(x,y,w,h)(x,y,w,h)(x,y,w,h) 都分开排列,而不是按照(x,y,w,h,c)(x,y,w,h,c)(x,y,w,h,c) 这样排列,存粹是为了后续计算时方便。

网络异常,图片无法展示
|

划分 7×77 \times 77×7 网格,共 98 个边界框,2 个框对应一个类别,所以 YOLOv1 只能在一个网格中检测出一个目标、单张图片最多预测 49 个目标。

2.2 Training

模型训练最重要的无非就是超参数的调整和损失函数的设计。

因为 YOLO 算法将检测问题看作是回归问题,所以自然地采用了比较容易优化的均方误差作为损失函数,但是面临定位误差和分类误差权重一样的问题;同时,在每张图像中,许多网格单元并不包含对象,即负样本(不包含物体的网格)远多于正样本(包含物体的网格),这通常会压倒了正样本的梯度,导致训练早期模型发散。

为了改善这点,引入了两个参数:λcoord=5\lambda_{coord}=5λcoord=5λnoobj=0.5\lambda_{noobj} =0.5λnoobj=0.5。对于边界框坐标预测损失(定位误差),采用较大的权重 λcoord=5\lambda_{coord} =5λcoord=5,然后区分不包含目标的边界框和含有目标的边界框,前者采用较小权重 λnoobj=0.5\lambda_{noobj} =0.5λnoobj=0.5。其他权重则均设为 0

对于大小不同的边界框,因为较小边界框的坐标误差比较大边界框要更敏感,所以为了部分解决这个问题,将网络的边界框的宽高预测改为对其平方根的预测,即预测值变为 (x,y,w,h)(x, y, \sqrt w, \sqrt h)(x,y,w,h)

YOLOv1 每个网格单元预测多个边界框。在训练时,每个目标我们只需要一个边界框预测器来负责。我们指定一个预测器“负责”根据哪个预测与真实值之间具有当前最高的 IOU 来预测目标。这导致边界框预测器之间的专业化。每个预测器可以更好地预测特定大小,方向角,或目标的类别,从而改善整体召回率。

YOLO 由于每个网格仅能预测 2 个边界框且仅可以包含一个类别,因此是对于一个单元格存在多个目标的问题,YOLO 只能选择一个来预测。这使得它在预测临近物体的数量上存在不足,如钢筋、人脸和鸟群检测等。

最终网络总的损失函数计算公式如下:

网络异常,图片无法展示
|

IijobjI_{ij}^{obj}Iijobj 指的是第 iii 个单元格存在目标,且该单元格中的第 jjj 个边界框负责预测该目标。 IiobjI_{i}^{obj}Iiobj 指的是第 iii 个单元格存在目标。

  • 前 2 行计算前景的 geo_loss(定位 loss)。
  • 第 3 行计算前景的 confidence_loss(包含目标的边界框的置信度误差项)。
  • 第 4 行计算背景的 confidence_loss
  • 第 5 行计算分类损失 class_loss

值得注意的是,对于不存在对应目标的边界框,其误差项就是只有置信度,坐标项误差是没法计算的。而只有当一个单元格内确实存在目标时,才计算分类误差项,否则该项也是无法计算的。

2.4. Inferences

同样采用了 NMS 算法来抑制多重检测,对应的模型推理结果解码代码如下,这里要和前面的 encoder 函数结合起来看。

# 对于网络输出预测 改为再图片上画出框及score
def decoder(pred):
    """
    pred (tensor)  torch.Size([1, 7, 7, 30])
    return (tensor) box[[x1,y1,x2,y2]] label[...]
    """
    grid_num = 7
    boxes = []
    cls_indexs = []
    probs = []
    cell_size = 1./grid_num
    pred = pred.data  # torch.Size([1, 14, 14, 30])
    pred = pred.squeeze(0)  # torch.Size([14, 14, 30])
    # 0 1      2 3   4    5 6   7 8   9
    # [中心坐标,长宽,置信度,中心坐标,长宽,置信度, 20个类别] x 7x7
    contain1 = pred[:, :, 4].unsqueeze(2)  # torch.Size([14, 14, 1])
    contain2 = pred[:, :, 9].unsqueeze(2)  # torch.Size([14, 14, 1])
    contain = torch.cat((contain1, contain2), 2)    # torch.Size([14, 14, 2])
    mask1 = contain > 0.1  # 大于阈值, torch.Size([14, 14, 2])  content: tensor([False, False])
    mask2 = (contain == contain.max())  # we always select the best contain_prob what ever it>0.9
    mask = (mask1+mask2).gt(0)
    # min_score,min_index = torch.min(contain, 2) # 每个 cell 只选最大概率的那个预测框
    for i in range(grid_num):
        for j in range(grid_num):
            for b in range(2):
                # index = min_index[i,j]
                # mask[i,j,index] = 0
                if mask[i, j, b] == 1:
                    box = pred[i, j, b*5:b*5+4]
                    contain_prob = torch.FloatTensor([pred[i, j, b*5+4]])
                    xy = torch.FloatTensor([j, i])*cell_size  # cell左上角  up left of cell
                    box[:2] = box[:2]*cell_size + xy  # return cxcy relative to image
                    box_xy = torch.FloatTensor(box.size())  # 转换成xy形式 convert[cx,cy,w,h] to [x1,y1,x2,y2]
                    box_xy[:2] = box[:2] - 0.5*box[2:]
                    box_xy[2:] = box[:2] + 0.5*box[2:]
                    max_prob, cls_index = torch.max(pred[i, j, 10:], 0)
                    if float((contain_prob*max_prob)[0]) > 0.1:
                        boxes.append(box_xy.view(1, 4))
                        cls_indexs.append(cls_index.item())
                        probs.append(contain_prob*max_prob)
    if len(boxes) == 0:
        boxes = torch.zeros((1, 4))
        probs = torch.zeros(1)
        cls_indexs = torch.zeros(1)
    else:
        boxes = torch.cat(boxes, 0)  # (n,4)
        # print(type(probs))
        # print(len(probs))
        # print(probs)
        probs = torch.cat(probs, 0)  # (n,)
        # print(probs)
        # print(type(cls_indexs))
        # print(len(cls_indexs))
        # print(cls_indexs)
        cls_indexs = torch.IntTensor(cls_indexs)  # (n,)
    # 去除冗余的候选框,得到最佳检测框(bbox)
    keep = nms(boxes, probs)
    # print("keep:", keep)
    a = boxes[keep]
    b = cls_indexs[keep]
    c = probs[keep]
    return a, b, c
复制代码

4.1 Comparison to Other Real-Time Systems

基于 GPU Titan X 硬件环境下,与他检测算法的性能比较如下。

网络异常,图片无法展示
|

5,代码实现思考

一些思考:快速的阅读了网上的一些 YOLOv1 代码实现,发现整个 YOLOv1 检测系统的代码可以分为以下几个部分:

  • 模型结构定义:特征提器模块 + 检测头模块(两个全连接层)。
  • 数据预处理,最难写的代码,需要对原有的 VOC 数据做预处理,编码成 YOLOv1 要求的格式输入,训练集的 labelshape(bach_size, 7, 7, 30)
  • 模型训练,主要由损失函数的构建组成,损失函数包括 5 个部分。
  • 模型预测,主要在于模型输出的解析,即解码成可方便显示的形式。

二,YOLOv2

YOLO9000CVPR2017 的最佳论文提名,但是这篇论文其实提出了 YOLOv2YOLO9000 两个模型,二者略有不同。前者主要是 YOLO 的升级版,后者的主要检测网络也是 YOLOv2,同时对数据集做了融合,使得模型可以检测 9000 多类物体。

摘要

YOLOv2 其实就是 YOLO9000,作者在 YOLOv1 基础上改进的一种新的 state-of-the-art 目标检测模型,它能检测多达 9000 个目标!利用了多尺度(multi-scale)训练方法,YOLOv2 可以在不同尺寸的图片上运行,并取得速度和精度的平衡。

在速度达到在 40 FPS 同时,YOLOv2 获得 78.6 mAP 的精度,性能优于backboneResNetFaster RCNNSSD 等当前最优(state-of-the-art) 模型。最后作者提出一种联合训练目标检测和分类的方法,基于这种方法,YOLO9000 能实时检测多达 9000 种目标。

YOLOv1 虽然速度很快,但是还有很多缺点:

  • 虽然每个 grid 预测两个框,但是只能对应一个目标,对于同一个 grid 有着两个目标的情况下,YOLOv1 是检测不全的,且模型最多检测 7×7=497 \times 7 = 497×7=49 个目标,即表现为模型查全率低。
  • 预测框不够准确,之前回归 (x,y,w,h)(x,y,w,h)(x,y,w,h) 的方法不够精确,即表现为模型精确率低。
  • 回归参数网络使用全连接层参数量太大,即模型检测头还不够块。

YOLOv2 的改进

1,中心坐标位置预测的改进

YOLOv1 模型预测的边界框中心坐标 (x,y)(x,y)(x,y) 是基于 grid 的偏移,这里 grid 的位置是固定划分出来的,偏移量 = 目标位置 - grid 的位置。

边界框的编码过程YOLOv2 参考了两阶段网络的 anchor boxes 来预测边界框相对先验框的偏移,同时沿用 YOLOv1 的方法预测边界框中心点相对于 grid 左上角位置的相对偏移值。(x,y,w,h)(x,y,w,h)(x,y,w,h) 的偏移值和实际坐标值的关系如下图所示。

网络异常,图片无法展示
|

各个字母的含义如下:

  • bx,by,bw,bhb_x,b_y,b_w,b_hbx,by,bw,bh :模型预测结果转化为 box 中心坐标和宽高后的值
  • tx,ty,tw,tht_x,t_y,t_w,t_htx,ty,tw,th :模型要预测的偏移量。
  • cx,cyc_x,c_ycx,cygrid 的左上角坐标,如上图所示。
  • pw,php_w,p_hpw,phanchor 的宽和高,这里的 anchor 是人为定好的一个框,宽和高是固定的。

通过以上定义我们从直接预测位置改为预测一个偏移量,即基于 anchor 框的宽高和 grid 的先验位置的偏移量,位置上使用 grid,宽高上使用 anchor 框,得到最终目标的位置,这种方法叫作 location prediction

预测偏移不直接预测位置,是因为作者发现直接预测位置会导致神经网络在一开始训练时不稳定,使用偏移量会使得训练过程更加稳定,性能指标提升了 5% 左右。

在数据集的预处理过程中,关键的边界框编码函数如下(代码来自 github,这个版本更清晰易懂):

def encode(self, boxes, labels, input_size):
    '''Encode target bounding boxes and class labels into YOLOv2 format.
    Args:
        boxes: (tensor) bounding boxes of (xmin,ymin,xmax,ymax) in range [0,1], sized [#obj, 4].
        labels: (tensor) object class labels, sized [#obj,].
        input_size: (int) model input size.
    Returns:
        loc_targets: (tensor) encoded bounding boxes, sized [5,4,fmsize,fmsize].
        cls_targets: (tensor) encoded class labels, sized [5,20,fmsize,fmsize].
        box_targets: (tensor) truth boxes, sized [#obj,4].
    '''
    num_boxes = len(boxes)
    # input_size -> fmsize
    # 320->10, 352->11, 384->12, 416->13, ..., 608->19
    fmsize = (input_size - 320) / 32 + 10
    grid_size = input_size / fmsize
    boxes *= input_size  # scale [0,1] -> [0,input_size]
    bx = (boxes[:,0] + boxes[:,2]) * 0.5 / grid_size  # in [0,fmsize]
    by = (boxes[:,1] + boxes[:,3]) * 0.5 / grid_size  # in [0,fmsize]
    bw = (boxes[:,2] - boxes[:,0]) / grid_size        # in [0,fmsize]
    bh = (boxes[:,3] - boxes[:,1]) / grid_size        # in [0,fmsize]
    tx = bx - bx.floor()
    ty = by - by.floor()
    xy = meshgrid(fmsize, swap_dims=True) + 0.5  # grid center, [fmsize*fmsize,2]
    wh = torch.Tensor(self.anchors)              # [5,2]
    xy = xy.view(fmsize,fmsize,1,2).expand(fmsize,fmsize,5,2)
    wh = wh.view(1,1,5,2).expand(fmsize,fmsize,5,2)
    anchor_boxes = torch.cat([xy-wh/2, xy+wh/2], 3)  # [fmsize,fmsize,5,4]
    ious = box_iou(anchor_boxes.view(-1,4), boxes/grid_size)  # [fmsize*fmsize*5,N]
    ious = ious.view(fmsize,fmsize,5,num_boxes)               # [fmsize,fmsize,5,N]
    loc_targets = torch.zeros(5,4,fmsize,fmsize)  # 5boxes * 4coords
    cls_targets = torch.zeros(5,20,fmsize,fmsize)
    for i in range(num_boxes):
        cx = int(bx[i])
        cy = int(by[i])
        _, max_idx = ious[cy,cx,:,i].max(0)
        j = max_idx[0]
        cls_targets[j,labels[i],cy,cx] = 1
        tw = bw[i] / self.anchors[j][0]
        th = bh[i] / self.anchors[j][1]
        loc_targets[j,:,cy,cx] = torch.Tensor([tx[i], ty[i], tw, th])
    return loc_targets, cls_targets, boxes/grid_size
复制代码

边界框的解码过程:虽然模型预测的是边界框的偏移量 (tx,ty,tw,th)(t_x,t_y,t_w,t_h)(tx,ty,tw,th),但是可通过以下公式计算出边界框的实际位置。

bx=σ(tx)+cxby=σ(ty)+cybw=pwetwbh=phethb_x = \sigma(t_x) + c_x \\\\ b_y = \sigma(t_y) + c_y \\\\ b_w = p_{w}e^{t_w} \\\\ b_h = p_{h}e^{t_h}bx=σ(tx)+cxby=σ(ty)+cybw=pwetwbh=pheth

其中,(cx,cy)(c_x, c_y)(cx,cy)grid 的左上角坐标,因为 σ\sigmaσ 表示的是 sigmoid 函数,所以边界框的中心坐标会被约束在 grid 内部,防止偏移过多。pwp_wpwphp_hph 是先验框(anchors)的宽度与高度,其值相对于特征图大小 W×HW\times HW×H = 13×1313\times 1313×13 而言的,因为划分为 13×1313 \times 1313×13grid,所以最后输出的特征图中每个 grid 的长和宽均是 1。知道了特征图的大小,就可以将边界框相对于整个特征图的位置和大小计算出来(均取值 0,1{0,1}0,1)。

bx=(σ(tx)+cx)/Wby=(σ(ty)+cy)/Hbw=pwetw/Wbh=pheth/Hb_x = (\sigma(t_x) + c_x)/W \\\\ b_y = (\sigma(t_y) + c_y)/H \\\\ b_w = p_{w}e^{t_w}/W \\\\ b_h = p_{h}e^{t_h}/Hbx=(σ(tx)+cx)/Wby=(σ(ty)+cy)/Hbw=pwetw/Wbh=pheth/H

在模型推理的时候,将以上 4 个值分别乘以图片的宽度和长度(像素点值)就可以得到边界框的实际中心坐标和大小。

在模型推理过程中,模型输出张量的解析,即边界框的解码函数如下:

def decode(self, outputs, input_size):
    '''Transform predicted loc/conf back to real bbox locations and class labels.
    Args:
        outputs: (tensor) model outputs, sized [1,125,13,13].
        input_size: (int) model input size.
    Returns:
        boxes: (tensor) bbox locations, sized [#obj, 4].
        labels: (tensor) class labels, sized [#obj,1].
    '''
    fmsize = outputs.size(2)
    outputs = outputs.view(5,25,13,13)
    loc_xy = outputs[:,:2,:,:]   # [5,2,13,13]
    grid_xy = meshgrid(fmsize, swap_dims=True).view(fmsize,fmsize,2).permute(2,0,1)  # [2,13,13]
    box_xy = loc_xy.sigmoid() + grid_xy.expand_as(loc_xy)  # [5,2,13,13]
    loc_wh = outputs[:,2:4,:,:]  # [5,2,13,13]
    anchor_wh = torch.Tensor(self.anchors).view(5,2,1,1).expand_as(loc_wh)  # [5,2,13,13]
    box_wh = anchor_wh * loc_wh.exp()  # [5,2,13,13]
    boxes = torch.cat([box_xy-box_wh/2, box_xy+box_wh/2], 1)  # [5,4,13,13]
    boxes = boxes.permute(0,2,3,1).contiguous().view(-1,4)    # [845,4]
    iou_preds = outputs[:,4,:,:].sigmoid()  # [5,13,13]
    cls_preds = outputs[:,5:,:,:]  # [5,20,13,13]
    cls_preds = cls_preds.permute(0,2,3,1).contiguous().view(-1,20)
    cls_preds = softmax(cls_preds)  # [5*13*13,20]
    score = cls_preds * iou_preds.view(-1).unsqueeze(1).expand_as(cls_preds)  # [5*13*13,20]
    score = score.max(1)[0].view(-1)  # [5*13*13,]
    print(iou_preds.max())
    print(cls_preds.max())
    print(score.max())
    ids = (score>0.5).nonzero().squeeze()
    keep = box_nms(boxes[ids], score[ids])  # NMS 算法去除重复框
    return boxes[ids][keep] / fmsize
复制代码

2,1 个 gird 只能对应一个目标的改进

或者说很多目标预测不到,查全率低的改进

YOLOv2 首先把 7×77 \times 77×7 个区域改为 13×1313 \times 1313×13grid(区域),每个区域有 5 个anchor,且每个 anchor 对应着 1 个类别,那么,输出的尺寸就应该为:[N,13,13,125]

125=5×(5+20)125 = 5 \times (5 + 20)125=5×(5+20)

网络异常,图片无法展示
|

值得注意的是之前 YOLOv1 的每个 grid 只能预测一个目标的分类概率值,两个 boxes 共享这个置信度概率。现在 YOLOv2 使用了 anchor 先验框后,每个 grid 的每个 anchor 都单独预测一个目标的分类概率值。

之所以每个 grid5anchor,是因为作者对 VOC/COCO 数据集进行 K-means 聚类实验,发现当 k=5 时,模型 recall vs. complexity 取得了较好的平衡。当然,kkk 越好,mAP 肯定越高,但是为了平衡模型复杂度,作者选择了 5 个聚类簇,即划分成 5 类先验框。设置先验框的主要目的是为了使得预测框与 ground truthIOU 更好,所以聚类分析时选用 box 与聚类中心 box 之间的 IOU 值作为距离指标:

d(box,centroid)=1−IOU(box,centroid)d(box, centroid) = 1-IOU(box, centroid)d(box,centroid)=1IOU(box,centroid)

Faster RCNN 手动设置 anchor 的大小和宽高比不同,YOLOv2 的 anchor 是从数据集中统计得到的。

3,backbone 的改进

作者提出了一个全新的 backbone 网络:Darknet-19,它是基于前人经典工作和该领域常识的基础上进行设计的。Darknet-19 网络和 VGG 网络类似,主要使用 3×33 \times 33×3 卷积,并且每个 2×22 \times 22×2pooling 操作之后将特征图通道数加倍。借鉴 NIN 网络的工作,作者使用 global average pooling 进行预测,并在 3×33 \times 33×3 卷积之间使用 1×11 \times 11×1 卷积来降低特征图通道数从而降低模型计算量和参数量。Darknet-19 网络的每个卷积层后面都是用了 BN 层来加快模型收敛,防止模型过拟合。

Darknet-19 网络总共有 19 个卷积层(convolution)、5 最大池化层(maxpooling)。Darknet-195.58 T的计算量在 ImageNet 数据集上取得了 72.9% 的 top-1 精度和 91.2% 的 top-5 精度。Darket19 网络参数表如下图所示。

网络异常,图片无法展示
|

检测训练。在 Darknet19 网络基础上进行修改后用于目标检测。首先,移除网络的最后一个卷积层,然后添加滤波器个数为 10243×33 \times 33×3 卷积层,最后添加一个 1×11 \times 11×1 卷积层,其滤波器个数为模型检测需要输出的变量个数。对于 VOC 数据集,每个 grid 预测 5 个边界框,每个边界框有 5 个坐标(tx,ty,tw,th 和 tot_x, t_y, t_w, t_h \ 和\ t_otx,ty,tw,thto)和 20 个类别,所以共有 125 个滤波器。我们还添加了从最后的 3×3×512 层到倒数第二层卷积层的直通层,以便模型可以使用细粒度特征。

Pr(object)∗IOU(b;object)=σ(to)P_r(object)*IOU(b; object) = \sigma (t_o)Pr(object)IOU(b;object)=σ(to)

Yolov2 整个模型结构代码如下:

代码来源 这里

'''Darknet in PyTorch.'''
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.nn.functional as F
from torch.autograd import Variable
class Darknet(nn.Module):
    # (64,1) means conv kernel size is 1, by default is 3.
    cfg1 = [32, 'M', 64, 'M', 128, (64,1), 128, 'M', 256, (128,1), 256, 'M', 512, (256,1), 512, (256,1), 512]  # conv1 - conv13
    cfg2 = ['M', 1024, (512,1), 1024, (512,1), 1024]  # conv14 - conv18
    def __init__(self):
        super(Darknet, self).__init__()
        self.layer1 = self._make_layers(self.cfg1, in_planes=3)
        self.layer2 = self._make_layers(self.cfg2, in_planes=512)
        #### Add new layers
        self.conv19 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
        self.bn19 = nn.BatchNorm2d(1024)
        self.conv20 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
        self.bn20 = nn.BatchNorm2d(1024)
        # Currently I removed the passthrough layer for simplicity
        self.conv21 = nn.Conv2d(1024, 1024, kernel_size=3, stride=1, padding=1)
        self.bn21 = nn.BatchNorm2d(1024)
        # Outputs: 5boxes * (4coordinates + 1confidence + 20classes)
        self.conv22 = nn.Conv2d(1024, 5*(5+20), kernel_size=1, stride=1, padding=0)
    def _make_layers(self, cfg, in_planes):
        layers = []
        for x in cfg:
            if x == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
            else:
                out_planes = x[0] if isinstance(x, tuple) else x
                ksize = x[1] if isinstance(x, tuple) else 3
                layers += [nn.Conv2d(in_planes, out_planes, kernel_size=ksize, padding=(ksize-1)//2),
                           nn.BatchNorm2d(out_planes),
                           nn.LeakyReLU(0.1, True)]
                in_planes = out_planes
        return nn.Sequential(*layers)
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = F.leaky_relu(self.bn19(self.conv19(out)), 0.1)
        out = F.leaky_relu(self.bn20(self.conv20(out)), 0.1)
        out = F.leaky_relu(self.bn21(self.conv21(out)), 0.1)
        out = self.conv22(out)
        return out
def test():
    net = Darknet()
    y = net(Variable(torch.randn(1,3,416,416)))
    print(y.size())  # 模型最后输出张量大小 [1,125,13,13]
if __name__ == "__main__":
    test()
复制代码

4,多尺度训练

YOLOv1 输入图像分辨率为 449×448449 \times 448449×448,因为使用了 anchor boxes,所以 YOLOv2 将输入分辨率改为 416×416416 \times 416416×416。又因为 YOLOv2 模型中只有卷积层和池化层,所以YOLOv2的输入可以不限于 416×416416 \times 416416×416 大小的图片。为了增强模型的鲁棒性,YOLOv2 采用了多尺度输入训练策略,具体来说就是在训练过程中每间隔一定的 iterations 之后改变模型的输入图片大小。由于 YOLOv2 的下采样总步长为 32,所以输入图片大小选择一系列为 32 倍数的值: {320,352,...,608}\lbrace 320, 352,...,608 \rbrace{320,352,...,608} ,因此输入图片分辨率最小为 320×320320\times 320320×320,此时对应的特征图大小为  10×1010\times 1010×10(不是奇数),而输入图片最大为 608×608608\times 608608×608 ,对应的特征图大小为 19×1919\times 1919×19 。在训练过程,每隔 10iterations随机选择一种输入图片大小,然后需要修最后的检测头以适应维度变化后,就可以重新训练。

采用 Multi-Scale Training 策略,YOLOv2 可以适应不同输入大小的图片,并且预测出很好的结果。在测试时,YOLOv2 可以采用不同大小的图片作为输入,在 VOC 2007 数据集上的测试结果如下图所示。

网络异常,图片无法展示
|

损失函数

YOLOv2 的损失函数的计算公式归纳如下

网络异常,图片无法展示
|

第 2,3 行:ttt 是迭代次数,即前 12800 步我们计算这个损失,后面不计算了。即前 12800 步我们会优化预测的 (x,y,w,h)(x,y,w,h)(x,y,w,h)anchor(x,y,w,h)(x,y,w,h)(x,y,w,h) 的距离 + 预测的 (x,y,w,h)(x,y,w,h)(x,y,w,h)GT(x,y,w,h)(x,y,w,h)(x,y,w,h) 的距离,12800 步之后就只优化预测的 (x,y,w,h)(x,y,w,h)(x,y,w,h)GT(x,y,w,h)(x,y,w,h)(x,y,w,h) 的距离,原因是这时的预测结果已经较为准确了,anchor已经满足检测系统的需要,而在一开始预测不准的时候,用上 anchor 可以加速训练。

YOLOv2 的损失函数实现代码如下,损失函数计算过程中的模型预测结果的解码函数和前面的解码函数略有不同,其包含关键部分目标 bbox 的解析。

from __future__ import print_function
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
from utils import box_iou, meshgrid
class YOLOLoss(nn.Module):
    def __init__(self):
        super(YOLOLoss, self).__init__()
    def decode_loc(self, loc_preds):
        '''Recover predicted locations back to box coordinates.
        Args:
          loc_preds: (tensor) predicted locations, sized [N,5,4,fmsize,fmsize].
        Returns:
          box_preds: (tensor) recovered boxes, sized [N,5,4,fmsize,fmsize].
        '''
        anchors = [(1.3221,1.73145),(3.19275,4.00944),(5.05587,8.09892),(9.47112,4.84053),(11.2364,10.0071)]
        N, _, _, fmsize, _ = loc_preds.size()
        loc_xy = loc_preds[:,:,:2,:,:]   # [N,5,2,13,13]
        grid_xy = meshgrid(fmsize, swap_dims=True).view(fmsize,fmsize,2).permute(2,0,1)  # [2,13,13]
        grid_xy = Variable(grid_xy.cuda())
        box_xy = loc_xy.sigmoid() + grid_xy.expand_as(loc_xy)  # [N,5,2,13,13]
        loc_wh = loc_preds[:,:,2:4,:,:]  # [N,5,2,13,13]
        anchor_wh = torch.Tensor(anchors).view(1,5,2,1,1).expand_as(loc_wh)  # [N,5,2,13,13]
        anchor_wh = Variable(anchor_wh.cuda())
        box_wh = anchor_wh * loc_wh.exp()  # [N,5,2,13,13]
        box_preds = torch.cat([box_xy-box_wh/2, box_xy+box_wh/2], 2)  # [N,5,4,13,13]
        return box_preds
    def forward(self, preds, loc_targets, cls_targets, box_targets):
        '''
        Args:
          preds: (tensor) model outputs, sized [batch_size,150,fmsize,fmsize].
          loc_targets: (tensor) loc targets, sized [batch_size,5,4,fmsize,fmsize].
          cls_targets: (tensor) conf targets, sized [batch_size,5,20,fmsize,fmsize].
          box_targets: (list) box targets, each sized [#obj,4].
        Returns:
          (tensor) loss = SmoothL1Loss(loc) + SmoothL1Loss(iou) + SmoothL1Loss(cls)
        '''
        batch_size, _, fmsize, _ = preds.size()
        preds = preds.view(batch_size, 5, 4+1+20, fmsize, fmsize)
        ### loc_loss
        xy = preds[:,:,:2,:,:].sigmoid()   # x->sigmoid(x), y->sigmoid(y)
        wh = preds[:,:,2:4,:,:].exp()
        loc_preds = torch.cat([xy,wh], 2)  # [N,5,4,13,13]
        pos = cls_targets.max(2)[0].squeeze() > 0  # [N,5,13,13]
        num_pos = pos.data.long().sum()
        mask = pos.unsqueeze(2).expand_as(loc_preds)  # [N,5,13,13] -> [N,5,1,13,13] -> [N,5,4,13,13]
        loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=False)
        ### iou_loss
        iou_preds = preds[:,:,4,:,:].sigmoid()  # [N,5,13,13]
        iou_targets = Variable(torch.zeros(iou_preds.size()).cuda()) # [N,5,13,13]
        box_preds = self.decode_loc(preds[:,:,:4,:,:])  # [N,5,4,13,13]
        box_preds = box_preds.permute(0,1,3,4,2).contiguous().view(batch_size,-1,4)  # [N,5*13*13,4]
        for i in range(batch_size):
            box_pred = box_preds[i]  # [5*13*13,4]
            box_target = box_targets[i]  # [#obj, 4]
            iou_target = box_iou(box_pred, box_target)  # [5*13*13, #obj]
            iou_targets[i] = iou_target.max(1)[0].view(5,fmsize,fmsize)  # [5,13,13]
        mask = Variable(torch.ones(iou_preds.size()).cuda()) * 0.1  # [N,5,13,13]
        mask[pos] = 1
        iou_loss = F.smooth_l1_loss(iou_preds*mask, iou_targets*mask, size_average=False)
        ### cls_loss
        cls_preds = preds[:,:,5:,:,:]  # [N,5,20,13,13]
        cls_preds = cls_preds.permute(0,1,3,4,2).contiguous().view(-1,20)  # [N,5,20,13,13] -> [N,5,13,13,20] -> [N*5*13*13,20]
        cls_preds = F.softmax(cls_preds)  # [N*5*13*13,20]
        cls_preds = cls_preds.view(batch_size,5,fmsize,fmsize,20).permute(0,1,4,2,3)  # [N*5*13*13,20] -> [N,5,20,13,13]
        pos = cls_targets > 0
        cls_loss = F.smooth_l1_loss(cls_preds[pos], cls_targets[pos], size_average=False)
        print('%f %f %f' % (loc_loss.data[0]/num_pos, iou_loss.data[0]/num_pos, cls_loss.data[0]/num_pos), end=' ')
        return (loc_loss + iou_loss + cls_loss) / num_pos
复制代码

YOLOv2VOC2007 数据集上和其他 state-of-the-art 模型的测试结果的比较如下曲线所示。

网络异常,图片无法展示
|

相关文章
|
2月前
|
机器学习/深度学习 人工智能 算法
模型无关的局部解释(LIME)技术原理解析及多领域应用实践
在当前数据驱动的商业环境中,人工智能(AI)和机器学习(ML)已成为各行业决策的关键工具,但随之而来的是“黑盒”问题:模型内部机制难以理解,引发信任缺失、监管合规难题及伦理考量。LIME(局部可解释模型无关解释)应运而生,通过解析复杂模型的个别预测,提供清晰、可解释的结果。LIME由华盛顿大学的研究者于2016年提出,旨在解决AI模型的透明度问题。它具有模型无关性、直观解释和局部保真度等优点,在金融、医疗等领域广泛应用。LIME不仅帮助企业提升决策透明度,还促进了模型优化和监管合规,是实现可解释AI的重要工具。
114 9
|
2月前
|
开发框架 供应链 监控
并行开发模型详解:类型、步骤及其应用解析
在现代研发环境中,企业需要在有限时间内推出高质量的产品,以满足客户不断变化的需求。传统的线性开发模式往往拖慢进度,导致资源浪费和延迟交付。并行开发模型通过允许多个开发阶段同时进行,极大提高了产品开发的效率和响应能力。本文将深入解析并行开发模型,涵盖其类型、步骤及如何通过辅助工具优化团队协作和管理工作流。
74 3
|
20天前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
35 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
1月前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
84 2
|
1月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
26天前
|
机器学习/深度学习 人工智能 自然语言处理
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
探索深度学习与自然语言处理的前沿技术:Transformer模型的深度解析
83 0
|
2月前
|
机器学习/深度学习 搜索推荐 大数据
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
【10月更文挑战第2天】在处理大规模数据集的推荐系统项目时,提高检索模型的召回率成为关键挑战。本文分享了通过改进特征工程(如加入用户活跃时段和物品相似度)和优化模型结构(引入注意力机制)来提升召回率的具体策略与实现代码。严格的A/B测试验证了新模型的有效性,为改善用户体验奠定了基础。这次实践加深了对特征工程与模型优化的理解,并为未来的技术探索提供了方向。
130 2
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
24 1
|
2月前
|
机器学习/深度学习 算法 Python
深度解析机器学习中过拟合与欠拟合现象:理解模型偏差背后的原因及其解决方案,附带Python示例代码助你轻松掌握平衡技巧
【10月更文挑战第10天】机器学习模型旨在从数据中学习规律并预测新数据。训练过程中常遇过拟合和欠拟合问题。过拟合指模型在训练集上表现优异但泛化能力差,欠拟合则指模型未能充分学习数据规律,两者均影响模型效果。解决方法包括正则化、增加训练数据和特征选择等。示例代码展示了如何使用Python和Scikit-learn进行线性回归建模,并观察不同情况下的表现。
468 3
|
1月前
|
安全 测试技术 Go
Go语言中的并发编程模型解析####
在当今的软件开发领域,高效的并发处理能力是提升系统性能的关键。本文深入探讨了Go语言独特的并发编程模型——goroutines和channels,通过实例解析其工作原理、优势及最佳实践,旨在为开发者提供实用的Go语言并发编程指南。 ####

热门文章

最新文章

推荐镜像

更多