一,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
帧,性能由于 DPM
和 R-CNN
等检测方法。
1. Introduction
之前的目标检测器是重用分类器来执行检测,为了检测目标,这些系统在图像上不断遍历一个框,并利用分类器去判断这个框是不是目标。像可变形部件模型(DPM
)使用互动窗口方法,其分类器在整个图像的均匀间隔的位置上运行。
作者将目标检测看作是单一的回归问题,直接从图像像素得到边界框坐标和类别概率。
YOLO 检测系统如图 1 所示。单个检测卷积网络可以同时预测多个目标的边界框和类别概率。YOLO
和传统的目标检测方法相比有诸多优点。
首先,YOLO
速度非常快,我们将检测视为回归问题,所以检测流程也简单。其次,YOLO
在进行预测时,会对图像进行全面地推理。第三,YOLO
模型具有泛化能力,其比 DPM
和R-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
个预测变量:xxx,yyy,www,hhh 和 confidence
。(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+5∗2)=1470。
总结:YOLO
系统将检测建模为回归问题。它将图像分成 S×SS \times SS×S 的 gird
,每个 grid
都会预测 BBB 个边界框,同时也包含 CCC 个类别的概率,这些预测对应的就是 S×S×(C+5∗B)S \times S \times (C + 5*B)S×S×(C+5∗B)。
这里其实就是在描述 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
编码的目标类别信息)进行处理,符合检测系统要求的输入形式。算法步骤如下:
- 计算目标框中心点坐标和宽高,并遍历各个目标框;
- 计算目标中心点落在哪个
grid
上,target
相应位置的两个框的置信度值设为1
,同时对应类别值也置为1
; - 计算目标中心所在
grid
(网格)的左上角相对坐标:ij*cell_size
,然后目标中心坐标相对于子网格左上角的偏移比例delta_xy
; - 最后将
target
对应网格位置的 (x,y,w,h)(x, y, w, h)(x,y,w,h) 分别赋相应wh
、delta_xy
值。
2.1. Network Design
YOLO
模型使用卷积神经网络来实现,卷积层负责从图像中提取特征,全连接层预测输出类别概率和坐标。
YOLO
的网络架构受 GooLeNet
图像分类模型的启发。网络有 24
个卷积层,最后面是 2
个全连接层。整个网络的卷积只有 1×11 \times 11×1 和 3×33 \times 33×3 卷积层,其中 1×11 \times 11×1 卷积负责降维 ,而不是 GoogLeNet
的 Inception
模块。
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
要求的格式输入,训练集的label
的shape
为(bach_size, 7, 7, 30)
。 - 模型训练,主要由损失函数的构建组成,损失函数包括
5
个部分。 - 模型预测,主要在于模型输出的解析,即解码成可方便显示的形式。
二,YOLOv2
YOLO9000
是CVPR2017
的最佳论文提名,但是这篇论文其实提出了YOLOv2
和YOLO9000
两个模型,二者略有不同。前者主要是YOLO
的升级版,后者的主要检测网络也是YOLOv2
,同时对数据集做了融合,使得模型可以检测9000
多类物体。
摘要
YOLOv2
其实就是 YOLO9000
,作者在 YOLOv1
基础上改进的一种新的 state-of-the-art
目标检测模型,它能检测多达 9000
个目标!利用了多尺度(multi-scale
)训练方法,YOLOv2
可以在不同尺寸的图片上运行,并取得速度和精度的平衡。
在速度达到在 40 FPS
同时,YOLOv2
获得 78.6 mAP
的精度,性能优于backbone
为 ResNet
的 Faster RCNN
和 SSD
等当前最优(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,cy :
grid
的左上角坐标,如上图所示。 - pw,php_w,p_hpw,ph :
anchor
的宽和高,这里的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_wpw、php_hph 是先验框(anchors
)的宽度与高度,其值相对于特征图大小 W×HW\times HW×H = 13×1313\times 1313×13 而言的,因为划分为 13×1313 \times 1313×13 个 grid
,所以最后输出的特征图中每个 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×13 个 grid
(区域),每个区域有 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
都单独预测一个目标的分类概率值。
之所以每个 grid
取 5
个 anchor
,是因为作者对 VOC/COCO
数据集进行 K-means 聚类实验,发现当 k=5
时,模型 recall vs. complexity 取得了较好的平衡。当然,kkk 越好,mAP
肯定越高,但是为了平衡模型复杂度,作者选择了 5
个聚类簇,即划分成 5
类先验框。设置先验框的主要目的是为了使得预测框与 ground truth
的 IOU
更好,所以聚类分析时选用 box
与聚类中心 box
之间的 IOU
值作为距离指标:
d(box,centroid)=1−IOU(box,centroid)d(box, centroid) = 1-IOU(box, centroid)d(box,centroid)=1−IOU(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-19
以 5.58
T的计算量在 ImageNet
数据集上取得了 72.9%
的 top-1 精度和 91.2%
的 top-5 精度。Darket19 网络参数表如下图所示。
检测训练。在 Darknet19
网络基础上进行修改后用于目标检测。首先,移除网络的最后一个卷积层,然后添加滤波器个数为 1024
的 3×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,th和to)和 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 。在训练过程,每隔 10
个 iterations
随机选择一种输入图片大小,然后需要修最后的检测头以适应维度变化后,就可以重新训练。
采用 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 复制代码
YOLOv2
在 VOC2007
数据集上和其他 state-of-the-art
模型的测试结果的比较如下曲线所示。