论文地址:YOLO9000: Better, Faster, Stronger
预测更准确(Better)
1) Batch Normalization
CNN在训练过程中网络每层输入的分布一直在改变, 会使训练过程难度加大,但可以通过normalize每层的输入解决这个问题。YOLO v2在每一个卷积层后添加batch normalization,通过这一方法,mAP获得了2%的提升。batch normalization 也有助于规范化模型,可以在舍弃dropout优化后依然不会过拟合。
2)High Resolution Classifier 使用高分辨率图像微调分类模型
mAP提升了3.7。
图像分类的训练样本很多,而标注了边框的用于训练对象检测的样本相比而言就比较少了,因为标注边框的人工成本比较高。所以对象检测模型通常都先用图像分类样本训练卷积层,提取图像特征。但这引出的另一个问题是,图像分类样本的分辨率不是很高。所以YOLO v1使用ImageNet的图像分类样本采用 224*224 作为输入,来训练CNN卷积层。然后在训练对象检测时,检测用的图像样本采用更高分辨率的 448*448 的图像作为输入。但这样切换对模型性能有一定影响。
所以YOLO2在采用 224*224 图像进行分类模型预训练后,再采用 448*448 的高分辨率样本对分类模型进行微调(10个epoch),使网络特征逐渐适应 448*448 的分辨率。然后再使用 448*448 的检测样本进行训练,缓解了分辨率突然切换造成的影响。
3) Convolutional With Anchor Boxes
借鉴Faster RCNN的做法,YOLO2也尝试采用先验框(anchor)。在每个grid预先设定一组不同大小和宽高比的边框,来覆盖整个图像的不同位置和多种尺度,这些先验框作为预定义的候选区在神经网络中将检测其中是否存在对象,以及微调边框的位置。
为了引入anchor boxes来预测bounding boxes,作者使用了一下几个技术手段:
(1)在网络中果断去掉了全连接层。
(2)去掉卷积网络部分的最后一个池化层。这是为了确保输出的卷积特征图有更高的分辨率;
(3)缩减网络输入。让图片输入分辨率从448*448降为416 * 416,这一步的目的是为了让后面产生的卷积特征图宽、高都为奇数,这样就可以产生一个center cell。
加入了anchor boxes后,可以预料到的结果是召回率上升,准确率轻微下降。
4) Dimension Clusters (聚类提取先验框尺度)
聚类提取先验框尺度,结合下面的约束预测边框的位置,使得mAP有4.8的提升。
之前先验框都是手工设定的,YOLO2尝试统计出更符合样本中对象尺寸的先验框,这样就可以减少网络微调先验框到实际位置的难度。YOLO2的做法是对训练集中标注的边框进行聚类分析,以寻找尽可能匹配样本的边框尺寸。
聚类算法最重要的是选择如何计算两个边框之间的“距离”,对于常用的欧式距离,大边框会产生更大的误差,但我们关心的是边框的IOU。所以,YOLO2在聚类时采用以下公式来计算两个边框之间的“距离”。
centroid是聚类时被选作中心的边框,box就是其它边框,d就是两者间的“距离”。IOU越大,“距离”越近。
5) Direct location prediction(约束预测边框的位置)
借鉴于Faster RCNN的先验框方法,在训练的早期阶段,其位置预测容易不稳定。其位置预测公式为:
其中, 是预测边框的中心,是先验框(anchor)的中心点坐标, 是先验框(anchor)的宽和高,是要学习的参数。 注意,YOLO论文中写的是 ,根据Faster RCNN,应该是"+"。
由于 的取值没有任何约束,因此预测边框的中心可能出现在任何位置,训练早期阶段不容易稳定。
YOLO调整了预测公式,将预测边框的中心约束在特定gird网格内。
其中, 是预测边框的中心和宽高。是预测边框的置信度,YOLO1是直接预测置信度的值,这里对预测参数 进行σ变换后作为置信度的值。是当前网格左上角到图像左上角的距离,要先将网格大小归一化,即令一个网格的宽=1,高=1。 是先验框的宽和高。 σ是sigmoid函数。 是要学习的参数,分别用于预测边框的中心和宽高,以及置信度。
参考上图,由于σ函数将约束在(0,1)范围内,所以根据上面的计算公式,预测边框的蓝色中心点被约束在蓝色背景的网格内。约束边框位置使得模型更容易学习,且预测更为稳定。
6)passthrough层检测细粒度特征
passthrough层检测细粒度特征使mAP提升1。
对象检测面临的一个问题是图像中对象会有大有小,输入图像经过多层网络提取特征,最后输出的特征图中(比如YOLO2中输入416*416经过卷积网络下采样最后输出是13*13),较小的对象可能特征已经不明显甚至被忽略掉了。为了更好的检测出一些比较小的对象,最后输出的特征图需要保留一些更细节的信息。
YOLO2引入一种称为passthrough层的方法在特征图中保留一些细节信息。具体来说,就是在最后一个pooling之前,特征图的大小是26*26*512,将其1拆4,直接传递(passthrough)到pooling后(并且又经过一组卷积)的特征图,两者叠加到一起作为输出的特征图。
具体怎样1拆4,图中示例的是1个4*4拆成4个2*2。因为深度不变,所以没有画出来。
另外,根据YOLO2的代码,特征图先用1*1卷积从 26*26*512 降维到 26*26*64,再做1拆4并passthrough。
7)多尺度图像训练
多尺度图像训练对mAP有1.4的提升。
因为去掉了全连接层,YOLO2可以输入任何尺寸的图像。因为整个网络下采样倍数是32,作者采用了{320,352,...,608}等10种输入图像的尺寸,这些尺寸的输入图像对应输出的特征图宽和高是{10,11,...19}。训练时每10个batch就随机更换一种尺寸,使网络能够适应各种大小的对象检测。
速度更快(Faster)
为了进一步提升速度,YOLO2提出了Darknet-19(有19个卷积层和5个MaxPooling层)网络结构。DarkNet-19比VGG-16小一些,精度不弱于VGG-16,但浮点运算量减少到约1/5,以保证更快的运算速度。
YOLO2的训练主要包括三个阶段。第一阶段就是先在ImageNet分类数据集上预训练Darknet-19,此时模型输入为 224*224 ,共训练160个epochs。然后第二阶段将网络的输入调整为 448*448 ,继续在ImageNet数据集上finetune分类模型,训练10个epochs,此时分类模型的top-1准确度为76.5%,而top-5准确度为93.3%。第三个阶段就是修改Darknet-19分类模型为检测模型,移除最后一个卷积层、global avgpooling层以及softmax层,并且新增了三个 3*3*1024卷积层,同时增加了一个passthrough层,最后使用 1*1 卷积层输出预测结果,输出的channels数为:num_anchors*(5+num_classes) ,和训练采用的数据集有关系。由于anchors数为5,对于VOC数据集(20种分类对象)输出的channels数就是125,最终的预测矩阵T的shape为 (batch_size, 13, 13, 125),可以先将其reshape为 (batch_size, 13, 13, 5, 25) ,其中 T[:, :, :, :, 0:4] 为边界框的位置和大小 ,T[:, :, :, :, 4] 为边界框的置信度,而 T[:, :, :, :, 5:] 为类别预测值。
对象检测模型各层的结构如下:
看一下passthrough层。图中第25层route 16,意思是来自16层的output,即26*26*512,这是passthrough层的来源(细粒度特征)。第26层1*1卷积降低通道数,从512降低到64(这一点论文在讨论passthrough的时候没有提到),输出26*26*64。第27层进行拆分(passthrough层)操作,1拆4分成13*13*256。第28层叠加27层和24层的输出,得到13*13*1280。后面再经过3*3卷积和1*1卷积,最后输出13*13*125。
YOLO2 输入->输出
综上所述,虽然YOLO2做出了一些改进,但总的来说网络结构依然很简单,就是一些卷积+pooling,从416*416*3 变换到 13*13*5*25。稍微大一点的变化是:
- 增加了batch normalization
- 增加了一个passthrough层
- 去掉了全连接层
- 采用了5个先验框
对比YOLO1的输出张量,YOLO2的主要变化就是会输出5个先验框,且每个先验框都会尝试预测一个对象。输出的 13*13*5*25 张量中,25维向量包含 20个对象的分类概率+4个边框坐标+1个边框置信度。
YOLO2 误差函数
误差依然包括边框位置误差、置信度误差、对象分类误差。
公式中:
意思是预测边框中,与真实对象边框IOU最大的那个,其IOU<阈值Thresh,此系数为1,即计入误差,否则为0,不计入误差。YOLO2使用Thresh=0.6。
意思是前128000次迭代计入误差。注意这里是与先验框的误差,而不是与真实对象边框的误差。可能是为了在训练早期使模型更快学会先预测先验框的位置。
意思是该边框负责预测一个真实对象(边框内有对象)。
各种 是不同类型误差的调节系数。
import math
import torch
import torch.nn as nn
class YoloLoss(nn.modules.loss._Loss):
# The loss I borrow from LightNet repo.
def __init__(self, num_classes, anchors, reduction=32, coord_scale=1.0, noobject_scale=1.0,
object_scale=5.0, class_scale=1.0, thresh=0.6):
super(YoloLoss, self).__init__()
self.num_classes = num_classes
self.num_anchors = len(anchors)
self.anchor_step = len(anchors[0])
self.anchors = torch.Tensor(anchors)
self.reduction = reduction
self.coord_scale = coord_scale
self.noobject_scale = noobject_scale
self.object_scale = object_scale
self.class_scale = class_scale
self.thresh = thresh
def forward(self, output, target):
"""
:param output: [batch_size, n_anchor_box*(n_classes+5), grid_size, grid_size]
:param target: 长度为batch_size的list,元素的shape为[n_targetbox, 5]
:return:
"""
batch_size = output.data.size(0)
height = output.data.size(2)
width = output.data.size(3)
# Get x,y,w,h,conf,cls
output = output.view(batch_size, self.num_anchors, -1, height * width) # [batch_size, n_anchor_box, n_class+5, grid_size*grid_size]
coord = torch.zeros_like(output[:, :, :4, :]) # [batch_size, n_anchor_box, 4, grid_size*grid_size]
coord[:, :, :2, :] = output[:, :, :2, :].sigmoid()
coord[:, :, 2:4, :] = output[:, :, 2:4, :]
conf = output[:, :, 4, :].sigmoid() # [batch_size, n_anchor_box, grid_size*grid_size]
cls = output[:, :, 5:, :].contiguous().view(batch_size * self.num_anchors, self.num_classes, # [batch_size*n_anchor_box*grid_size*grid_size, n_classes]
height * width).transpose(1, 2).contiguous().view(-1,
self.num_classes)
# Create prediction boxes
pred_boxes = torch.FloatTensor(batch_size * self.num_anchors * height * width, 4) # [batch_size*n_anchor_box*grid_size*grid_size, 4]
lin_x = torch.range(0, width - 1).repeat(height, 1).view(height * width) # [grid_size*grid_size]
lin_y = torch.range(0, height - 1).repeat(width, 1).t().contiguous().view(height * width) # [grid_size*grid_size]
anchor_w = self.anchors[:, 0].contiguous().view(self.num_anchors, 1) # [n_anchor_box, 1]
anchor_h = self.anchors[:, 1].contiguous().view(self.num_anchors, 1) # [n_anchor_box, 1]
if torch.cuda.is_available():
pred_boxes = pred_boxes.cuda()
lin_x = lin_x.cuda()
lin_y = lin_y.cuda()
anchor_w = anchor_w.cuda()
anchor_h = anchor_h.cuda()
pred_boxes[:, 0] = (coord[:, :, 0].detach() + lin_x).view(-1)
pred_boxes[:, 1] = (coord[:, :, 1].detach() + lin_y).view(-1)
pred_boxes[:, 2] = (coord[:, :, 2].detach().exp() * anchor_w).view(-1)
pred_boxes[:, 3] = (coord[:, :, 3].detach().exp() * anchor_h).view(-1)
pred_boxes = pred_boxes.cpu() # [batch_size*n_anchor_box*grid_size*grid_size, 4]
# Get target values
coord_mask, conf_mask, cls_mask, tcoord, tconf, tcls = self.build_targets(pred_boxes, target, height, width)
coord_mask = coord_mask.expand_as(tcoord) # [batch_size, n_anchor_box, 4, grid_size*grid_size]
tcls = tcls[cls_mask].view(-1).long()
cls_mask = cls_mask.view(-1, 1).repeat(1, self.num_classes)
if torch.cuda.is_available():
tcoord = tcoord.cuda()
tconf = tconf.cuda()
coord_mask = coord_mask.cuda()
conf_mask = conf_mask.cuda()
tcls = tcls.cuda()
cls_mask = cls_mask.cuda()
conf_mask = conf_mask.sqrt()
cls = cls[cls_mask].view(-1, self.num_classes)
# Compute losses
mse = nn.MSELoss(size_average=False)
ce = nn.CrossEntropyLoss(size_average=False)
self.loss_coord = self.coord_scale * mse(coord * coord_mask, tcoord * coord_mask) / batch_size
self.loss_conf = mse(conf * conf_mask, tconf * conf_mask) / batch_size
self.loss_cls = self.class_scale * 2 * ce(cls, tcls) / batch_size
self.loss_tot = self.loss_coord + self.loss_conf + self.loss_cls
return self.loss_tot, self.loss_coord, self.loss_conf, self.loss_cls
def build_targets(self, pred_boxes, ground_truth, height, width):
"""
:param pred_boxes: [batch_size*grid_size*grid_size*n_anchorbox, 4]
:param ground_truth: 长度为batch_size的list,元素的shape为[n_targetbox, 5]
:param height: grid_size
:param width: grid_size
:return:
"""
batch_size = len(ground_truth)
conf_mask = torch.ones(batch_size, self.num_anchors, height * width, requires_grad=False) * self.noobject_scale
coord_mask = torch.zeros(batch_size, self.num_anchors, 1, height * width, requires_grad=False)
cls_mask = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False).byte()
tcoord = torch.zeros(batch_size, self.num_anchors, 4, height * width, requires_grad=False)
tconf = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False)
tcls = torch.zeros(batch_size, self.num_anchors, height * width, requires_grad=False)
for b in range(batch_size):
if len(ground_truth[b]) == 0:
continue
# Build up tensors
cur_pred_boxes = pred_boxes[
b * (self.num_anchors * height * width):(b + 1) * (self.num_anchors * height * width)] # [n_anchorbox*grid_size*grid_size, 4]
if self.anchor_step == 4:
anchors = self.anchors.clone()
anchors[:, :2] = 0
else:
anchors = torch.cat([torch.zeros_like(self.anchors), self.anchors], 1) # [n_ancorbox, 4] x,y,w,h
gt = torch.zeros(len(ground_truth[b]), 4) # [n_target_box, 4] x,y,w,h
for i, anno in enumerate(ground_truth[b]):
gt[i, 0] = (anno[0] + anno[2] / 2) / self.reduction
gt[i, 1] = (anno[1] + anno[3] / 2) / self.reduction
gt[i, 2] = anno[2] / self.reduction
gt[i, 3] = anno[3] / self.reduction
# Set confidence mask of matching detections to 0
iou_gt_pred = bbox_ious(gt, cur_pred_boxes) # [n_target_box, grid_size*grid_size*n_anchorbox]
mask = (iou_gt_pred > self.thresh).sum(0) >= 1 # [n_anchorbox*grid_size*grid_size]
conf_mask[b][mask.view_as(conf_mask[b])] = 0 # conf_mask [batch_size, n_anchor_box, grid_size*grid_size]
# Find best anchor for each ground truth
gt_wh = gt.clone()
gt_wh[:, :2] = 0
iou_gt_anchors = bbox_ious(gt_wh, anchors) # [n_target_box, n_anchor_box]
_, best_anchors = iou_gt_anchors.max(1) # [n_target_box], 值为iou最大的anchor box 的index
# Set masks and target values for each ground truth
for i, anno in enumerate(ground_truth[b]):
gi = min(width - 1, max(0, int(gt[i, 0])))
gj = min(height - 1, max(0, int(gt[i, 1])))
best_n = best_anchors[i]
iou = iou_gt_pred[i][best_n * height * width + gj * width + gi]
coord_mask[b][best_n][0][gj * width + gi] = 1
cls_mask[b][best_n][gj * width + gi] = 1
conf_mask[b][best_n][gj * width + gi] = self.object_scale
tcoord[b][best_n][0][gj * width + gi] = gt[i, 0] - gi
tcoord[b][best_n][1][gj * width + gi] = gt[i, 1] - gj
tcoord[b][best_n][2][gj * width + gi] = math.log(max(gt[i, 2], 1.0) / self.anchors[best_n, 0])
tcoord[b][best_n][3][gj * width + gi] = math.log(max(gt[i, 3], 1.0) / self.anchors[best_n, 1])
tconf[b][best_n][gj * width + gi] = iou
tcls[b][best_n][gj * width + gi] = int(anno[4])
return coord_mask, conf_mask, cls_mask, tcoord, tconf, tcls
def bbox_ious(boxes1, boxes2):
b1x1, b1y1 = (boxes1[:, :2] - (boxes1[:, 2:4] / 2)).split(1, 1)
b1x2, b1y2 = (boxes1[:, :2] + (boxes1[:, 2:4] / 2)).split(1, 1)
b2x1, b2y1 = (boxes2[:, :2] - (boxes2[:, 2:4] / 2)).split(1, 1)
b2x2, b2y2 = (boxes2[:, :2] + (boxes2[:, 2:4] / 2)).split(1, 1)
dx = (b1x2.min(b2x2.t()) - b1x1.max(b2x1.t())).clamp(min=0)
dy = (b1y2.min(b2y2.t()) - b1y1.max(b2y1.t())).clamp(min=0)
intersections = dx * dy
areas1 = (b1x2 - b1x1) * (b1y2 - b1y1)
areas2 = (b2x2 - b2x1) * (b2y2 - b2y1)
unions = (areas1 + areas2.t()) - intersections
return intersections / unions
参考: