1. 概述
本文介绍下 YOLO 这种目标检测算法的历史,以及 YOLO 版本的演进。
2. YOLO v1
2.1. 介绍
YOLO v1 是 2016 年提出的 one-stage 目标检测网络,全称为 You Only Look Once 。
YOLO 的主要思想是将一幅图像分成 SxS 个网格 (grid cell) ,如果某个 object 的中心落在这个网格中,则这个网格就负责预测这个 object 。每个网格要预测 B 个 bounding box ,每个 bounding box 除了要预测位置之外,还要附带预测一个 confidence 值。每个网格还要预测 C 个类别的分数。
对于 Pascal VOC 数据集,假如我们使用网格的大小 S=7 , B=2 , Pascal VOC 数据集的类别个数 C=20 ,那么我们预测器的维度为 7x7x(2*(4+1)+20) 。每一个 bounding box 包含 5 个预测值, (x,y,h,w) 和 confidence , (x,y) 坐标表示相对于该网格的边界内的中心点坐标。 (h,w) 相对于整张图片的比例。 confidence 表示预测的边界框包含一个物体的置信度有多高以及该预测边界框的准确率。所以这五个参数都是在 [0-1] 范围内。
2.2. 主干网络
# input (224, 224)
#P1
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=3) # output of conv1 (size) = (batch_size, 64, 224, 224)
self.mp1 = nn.MaxPool2d(kernel_size=2, stride=2) # output size = (batch_size, 64, 112, 112)
#P2
self.conv2 = nn.Conv2d(64, 192, 3, 1, 1) # (b_s, 192, 112, 112)
self.mp2 = nn.MaxPool2d(2, 2) # (b_s, 192, 56, 56)
#P3
self.conv3 = nn.Conv2d(192, 128, 1, 1, 0) # (b_s, 128, 56, 56)
self.conv4 = nn.Conv2d(128, 256, 3, 1, 1) # (b_s, 256, 56, 56)
self.conv5 = nn.Conv2d(256, 256, 1, 1, 0) # (b_s, 256, 56, 56)
self.conv6 = nn.Conv2d(256, 512, 3, 1, 1) # (b_s, 512, 56, 56)
self.mp3 = nn.MaxPool2d(2, 2) # (b_s, 512, 28, 28)
#P4
self.conv7 = nn.Conv2d(512, 256, 1, 1, 0) # (b_s, 256, 28, 28) rd=1
self.conv8 = nn.Conv2d(256, 512, 3, 1, 1) # (b_s, 512, 28, 28) rd=1
self.conv9 = nn.Conv2d(512, 256, 1, 1, 0) # (b_s, 256, 28, 28) rd=2
self.conv10 = nn.Conv2d(256, 512, 3, 1, 1) # (b_s, 512, 28, 28) rd=2
self.conv11 = nn.Conv2d(512, 256, 1, 1, 0) # (b_s, 256, 28, 28) rd=3
self.conv12 = nn.Conv2d(256, 512, 3, 1, 1) # (b_s, 512, 28, 28) rd=3
self.conv13 = nn.Conv2d(512, 256, 1, 1, 0) # (b_s, 256, 28, 28) rd=4
self.conv14 = nn.Conv2d(256, 512, 3, 1, 1) # (b_s, 512, 28, 28) rd=4
self.conv15 = nn.Conv2d(512, 512, 1, 1, 0) # (b_s, 512, 28, 28)
self.conv16 = nn.Conv2d(512, 1024, 3, 1, 1) # (b_s, 1024, 28, 28)
self.mp4 = nn.MaxPool2d(2, 2) # (b_s, 1024, 14, 14)
#P5
self.conv17 = nn.Conv2d(1024, 512, 1, 1, 0) # (b_s, 512, 14, 14) rd=1
self.conv18 = nn.Conv2d(512, 1024, 3, 1, 1) # (b_s, 1024, 14, 14) rd=1
self.conv19 = nn.Conv2d(1024, 512, 1, 1, 0) # (b_s, 512, 14, 14) rd=2
self.conv20 = nn.Conv2d(512, 1024, 3, 1, 1) # (b_s, 1024, 14, 14) rd=2
self.conv21 = nn.Conv2d(1024, 1024, 1, 1, 0) # (b_s, 1024, 14, 14)
self.conv22 = nn.Conv2d(1024, 1024, 3, 2, 1) # (b_s, 1024, 7, 7)
#P6
self.conv23 = nn.Conv2d(1024, 1024, 3, 1, 1) # (b_s, 1024, 7, 7)
self.conv24 = nn.Conv2d(1024, 1024, 3, 1, 1) # (b_s, 1024, 7, 7)
#p7
self.fc1 = nn.Linear(1024*7*7, 4096)
#P8
self.fc2 = nn.Linear(4096, 30*7*7)
#LeakyReLU
self.L_ReLU = nn.LeakyReLU(0.1)
# Dropout
self.dropout = nn.Dropout(p=0.5)
2.3. v1 版本网络存在的问题
首先, v1 版本网络对一堆小目标检测效果不理想,比如一群鸟,因为 v1 版本中一个网格只检测两个目标。其次,模型很难泛化到新的、不常见的预测目标比例。最后,模型的损失函数会同样的对待小边界框与大边界框的误差,大边界框的小误差通常是正常的,但小边界框的小误差对 IOU 的影响要大得多。
3. YOLO v2
3.1. YOLO v2 相比 v1 版本的改进
YOLO v2 是 2017 年提出的,主要对 YOLO v1 网络基础上增加一些 trick 。
在 v2 版本的网络增加了 Batch Norm 层,去掉了 Dropout 层。
使用更高分辨率的图像数据。图像分类的训练样本很多,而标注了边框的用于训练对象检测的样本相比而言就比较少了,因为标注边框的人工成本比较高。所以对象检测模型通常都先用图像分类样本训练卷积层,提取图像特征。但这引出的另一个问题是,图像分类样本的分辨率不是很高。所以 YOLO v1 使用 ImageNet 的图像分类样本采用 224x224 作为输入,来训练 CNN 卷积层。然后在训练对象检测时,检测用的图像样本采用更高分辨率的 448x448 的图像作为输入。但这样切换对模型性能有一定影响。所以 YOLO v2 在采用 224x224 图像进行分类模型预训练后,再采用 448x448 的高分辨率样本对分类模型进行微调,使网络特征逐渐适应 448x448 的分辨率。然后再使用 448x448 的检测样本进行训练,缓解了分辨率突然切换造成的影响。
Convolutional With Anchor Boxes ,使用 Anchors ,大幅提升网络的召回率。作者去掉了后面的一个池化层以确保输出的卷积特征图有更高的分辨率。然后,通过缩减网络,让图片输入分辨率为 416x416,这一步的目的是为了让后面产生的卷积特征图宽高都为奇数,这样就可以产生一个 center cell 。作者观察到,大物体通常占据了图像的中间位置, 就可以只用中心的一个 cell 来预测这些物体的位置,否则就要用中间的 4 个 cell 来进行预测,这个技巧可稍稍提升效率。最后, YOLOv2 使用了卷积层降采样 (factor 为 32) ,使得输入卷积网络的 416x416 图片最终得到 13x13 的卷积特征图 (416/32=13) 。加入了 anchor boxes 后,可以预料到的结果是召回率上升,准确率下降。假设每个 cell 预测 9 个建议框,那么总共会预测 13 * 13 * 9 = 1521 个 boxes ,而之前的网络仅仅预测 7 * 7 * 2 = 98 个 boxes 。具体数据为:没有 anchor boxes ,模型 recall 为 81% , mAP 为 69.5% ;加入 anchor boxes ,模型 recall 为 88% , mAP为 69.2% 。这样看来,准确率只有小幅度的下降,而召回率则提升了 7% ,说明可以通过进一步的工作来加强准确率,的确有改进空间。
Dimension Clusters ,使用聚类算法生成 Anchors 。对训练集中标注的边框进行聚类分析,以寻找尽可能匹配样本的边框尺寸。聚类算法最重要的是选择如何计算两个边框之间的“距离”,对于常用的欧式距离,大边框会产生更大的误差,但我们关心的是边框的 IOU 。所以, YOLO v2 在聚类时采用以下公式来计算两个边框之间的“距离”。 d(box, centroid)= 1-IOU(box, centroid) , centroid 是聚类时被选作中心的边框, box 就是其它边框, d 就是两者间的“距离”。 IOU 越大,“距离”越近。
Direct location prediction ,约束预测边框的位置。借鉴于 Faster RCNN 的先验框方法,在训练的早期阶段,其位置预测容易不稳定。其位置预测公式为:
其中, x,y 是预测边框的中心, xa,ya 是先验框(anchor)的中心点坐标, wa,ha 是先验框(anchor)的宽和高, tx,ty 是要学习的参数。由于 tx,ty 的取值没有任何约束,因此预测边框的中心可能出现在任何位置,训练早期阶段不容易稳定。YOLO v2 调整了预测公式,将预测边框的中心约束在特定 gird 网格内。
其中, bx,by,bw,bh 是预测边框的中心和宽高。 pr(object)*iou(b,object) 是预测边框的置信度, YOLO v1 是直接预测置信度的值,这里对预测参数 to 进行 σ 变换后作为置信度的值。 cx,cy 是当前网格左上角到图像左上角的距离,要先将网格大小归一化,即令一个网格的宽=1,高=1。 pw,ph 是先验框的宽和高。 σ 是 sigmoid 函数。 tx,ty,th,tw,to 是要学习的参数,分别用于预测边框的中心和宽高,以及置信度。由于 σ 函数将 tx,ty 约束在 (0,1) 范围内,所以根据上面的计算公式,预测边框的中心点被约束在网格内。约束边框位置使得模型更容易学习,且预测更为稳定。
Fine-Grained Features (细粒度特征), passthrough 层,对象检测面临的一个问题是图像中对象会有大有小,输入图像经过多层网络提取特征,最后输出的特征图中(比如 YOLO v2 中输入 416x416 经过卷积网络下采样最后输出是 13x13 ),较小的对象可能特征已经不明显甚至被忽略掉了。为了更好的检测出一些比较小的对象,最后输出的特征图需要保留一些更细节的信息。 YOLO v2 引入一种称为 passthrough 层的方法在特征图中保留一些细节信息。具体来说,就是在最后一个 pooling 之前,特征图的大小是 26x26x512 ,将其 1 拆 4 ,直接传递( passthrough )到 pooling 后(并且又经过一组卷积)的特征图,两者叠加到一起作为输出的特征图。经过 passthrough 层的特征宽高变为原来的一半,通道数变为原来的四倍,然后与经过 pooling 层输出的特征进行 concat 处理。
多尺度图像训练,因为去掉了全连接层, YOLO v2 可以输入任何尺寸的图像。因为整个网络下采样倍数是 32 ,作者采用了 {320,352,…,608} 等 10 种输入图像的尺寸,这些尺寸的输入图像对应输出的特征图宽和高是 {10,11,…19} 。训练时每 10 个 batch 就随机更换一种尺寸,使网络能够适应各种大小的对象检测。
增强网络的预测能力,每个网格预测 5 个 boxes 和 5 个 confidence 。
YOLO v2 使用 Darknet-19 网络结构,相比较 VGG-16 参数减少,精度相当。
Darknet-19 网络实现:
- ReorgLayer 实现了 passthrough 层
def _make_layers(in_channels, net_cfg):
layers = []
if len(net_cfg) > 0 and isinstance(net_cfg[0], list):
for sub_cfg in net_cfg:
layer, in_channels = _make_layers(in_channels, sub_cfg)
layers.append(layer)
else:
for item in net_cfg:
if item == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
out_channels, ksize = item
layers.append(net_utils.Conv2d_BatchNorm(in_channels,
out_channels,
ksize,
same_padding=True))
# layers.append(net_utils.Conv2d(in_channels, out_channels,
# ksize, same_padding=True))
in_channels = out_channels
return nn.Sequential(*layers), in_channels
class Darknet19(nn.Module):
def __init__(self):
super(Darknet19, self).__init__()
net_cfgs = [
# conv1s
[(32, 3)],
['M', (64, 3)],
['M', (128, 3), (64, 1), (128, 3)],
['M', (256, 3), (128, 1), (256, 3)],
['M', (512, 3), (256, 1), (512, 3), (256, 1), (512, 3)],
# conv2
['M', (1024, 3), (512, 1), (1024, 3), (512, 1), (1024, 3)],
# conv3
[(1024, 3), (1024, 3)],
# conv4
[(1024, 3)]
]
# darknet
self.conv1s, c1 = _make_layers(3, net_cfgs[0:5])
self.conv2, c2 = _make_layers(c1, net_cfgs[5])
# ---
self.conv3, c3 = _make_layers(c2, net_cfgs[6])
stride = 2
# stride*stride times the channels of conv1s
self.reorg = ReorgLayer(stride=2)
# cat [conv1s, conv3]
self.conv4, c4 = _make_layers((c1*(stride*stride) + c3), net_cfgs[7])
# linear
out_channels = cfg.num_anchors * (cfg.num_classes + 5)
self.conv5 = net_utils.Conv2d(c4, out_channels, 1, 1, relu=False)
def forward(self, im_data, gt_boxes=None, gt_classes=None, dontcare=None,
size_index=0):
conv1s = self.conv1s(im_data)
conv2 = self.conv2(conv1s)
conv3 = self.conv3(conv2)
conv1s_reorg = self.reorg(conv1s)
cat_1_3 = torch.cat([conv1s_reorg, conv3], 1)
conv4 = self.conv4(cat_1_3)
conv5 = self.conv5(conv4) # batch_size, out_channels, h, w
3. YOLO v3
YOLO v3 使用了 Darknet-53 主干特征提取网络,引入 FPN 结构,在三个特征层上进行预测。
Darknet-53 网络实现如下:
- 相比较 Darknet-19 网络 Darknet-53 网络增加了残差层。
def darknet53(num_classes): return Darknet53(DarkResidualBlock, num_classes)
conv_batch 函数实现了 conv2d 、 BN 、 LeakyReLU 的堆叠。
def conv_batch(in_num, out_num, kernel_size=3, padding=1, stride=1): return nn.Sequential( nn.Conv2d(in_num, out_num, kernel_size=kernel_size, stride=stride, padding=padding, bias=False), nn.BatchNorm2d(out_num), nn.LeakyReLU())
残差层,降输入的特征经过 1x1 和 3x3 卷积后与输入特征相加, 1x1 卷积对特征层降维,然后通过 3x3 卷积再次将特征层升维到与输入一致,然后将两者进行相加。
class DarkResidualBlock(nn.Module): def __init__(self, in_channels): super(DarkResidualBlock, self).__init__() reduced_channels = int(in_channels/2) self.layer1 = conv_batch(in_channels, reduced_channels, kernel_size=1, padding=0) self.layer2 = conv_batch(reduced_channels, in_channels) def forward(self, x): residual = x out = self.layer1(x) out = self.layer2(out) out += residual return out
Darknet53 网络就是卷积层和残差层堆叠而成,最后通过全链接层降输出固定到类别的维度上。 num_blocks 参数表示残差层的重复次数。
class Darknet53(nn.Module): def __init__(self, block, num_classes): super(Darknet53, self).__init__() self.num_classes = num_classes self.conv1 = conv_batch(3, 32) self.conv2 = conv_batch(32, 64, stride=2) self.residual_block1 = self.make_layer(block, in_channels=64, num_blocks=1) self.conv3 = conv_batch(64, 128, stride=2) self.residual_block2 = self.make_layer(block, in_channels=128, num_blocks=2) self.conv4 = conv_batch(128, 256, stride=2) self.residual_block3 = self.make_layer(block, in_channels=256, num_blocks=8) self.conv5 = conv_batch(256, 512, stride=2) self.residual_block4 = self.make_layer(block, in_channels=512, num_blocks=8) self.conv6 = conv_batch(512, 1024, stride=2) self.residual_block5 = self.make_layer(block, in_channels=1024, num_blocks=4) self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(1024, self.num_classes) def forward(self, x): out = self.conv1(x) out = self.conv2(out) out = self.residual_block1(out) out = self.conv3(out) out = self.residual_block2(out) out = self.conv4(out) out = self.residual_block3(out) out = self.conv5(out) out = self.residual_block4(out) out = self.conv6(out) out = self.residual_block5(out) out = self.global_avg_pool(out) out = out.view(-1, 1024) out = self.fc(out) return out def make_layer(self, block, in_channels, num_blocks): layers = [] for i in range(0, num_blocks): layers.append(block(in_channels)) return nn.Sequential(*layers)
3. YOLO v3 以上版本使用的 trick
v3 以上的版本主要是增加一些 trick ,下面介绍下各个 trick 的原理以及实现。
3.1. Mosaic 图像增强
mosaic 数据增强参考了 CutMix 数据增强方式, CutMix 的处理方式比较简单,对于 A , B 两张图片,先随机生成一个裁剪框 Box ,裁剪掉 A 图的相应位置,然后用 B 图片相应位置的 ROI 放到 A 图中被裁剪的区域形成新的样本。
mosaic 数据增强则利用了四张图片,对四张图片进行拼接,每一张图片都有其对应的框框,将四张图片拼接之后就获得一张新的图片,同时也获得这张图片对应的框框,然后我们将这样一张新的图片传入到神经网络当中去学习,相当于一下子传入四张图片进行学习了。论文中说这极大丰富了检测物体的背景,且在标准化 BN 计算的时候一下子会计算四张图片的数据。
mosaic 数据增强实现过程如下:
- 从数据集中每次随机读取四张图片
- 分别对四张图片进行翻转、缩放、色域变化等操作
- 将原始图片按照第一张图片摆放在左上,第二张图片摆放在左下,第三张图片摆放在右下,第四张图片摆放在右上四个方向位置摆好
- 进行图片的组合和框的组合
3.2. SPP 模块
SPP 网络实现了不同尺度的特征融合。 SPP 模块的框架图如下图所示:
从上图可以看出 SPP 模块将输入特征与经过多个步长为 1 ,池化核大小为奇数的最大池化层的输出进行拼接。在 YOLOv3-SPP 网络中使用的池化核大小是 5 , 9 , 13 。
3.3. IOU 损失
介绍 IOU LOSS 之前我们先看一下 smooth L1 LOSS 有哪些缺点:
- 检测评价的方式是使用 IoU ,而实际回归坐标框的时候是使用 4 个坐标点,而当 smooth L1 LOSS 损失相同的情况下,IOU 却不同
- 通过 4 个点回归坐标框的方式是假设 4 个坐标点是相互独立的,没有考虑其相关性,实际 4 个坐标点具有一定的相关性
- 基于 smooth L1 LOSS 的距离的 loss 对于尺度不具有不变性
基于此提出 IoU Loss ,其将 4 个点构成的 box 看成一个整体进行回归, IOU Loss 也有很多衍生版本,包括 GIou Loss , DIOU Loss , CIOU Loss 。
3.3.1 IOU Loss
IOU Loss 具有能够更好的反应重合程度和具有尺度不变性等优点,但是当两个边界框不相交时损失为 0 。
3.3.2 GIou Loss
为了解决 IOU Loss 的问题,又提出了 GIOU Loss ,公式如下:
先计算两个框的最小闭包区域面积 Ac , 再计算出IoU,再计算闭包区域中不属于两个框的区域占闭包区域的比重,最后用 IoU 减去这个比重得到 GIoU 。计算损失时是 1-GIOU ,由于 GIOU 的取值范围时 [-1, 1] ,所以损失的取值范围为 [0, 2] 。
GIOU Loss 的缺点是当在某些情况下退化成了 IOU Loss 。
3.3.2 DIOU Loss
DIoU要比GIou更加符合目标框回归的机制,将目标与anchor之间的距离,重叠率以及尺度都考虑进去,使得目标框回归变得更加稳定,不会像 IoU 和 GIoU 一样出现训练过程中发散等问题。
上述公式右侧的分子代表的是计算两个中心点间的欧式距离。 分母代表的是能够同时包含预测框和真实框的最小闭包区域的对角线距离。
计算损失时是 1-DIOU ,由于 DIOU 的取值范围时 [-1, 1] ,所以损失的取值范围为 [0, 2] 。
3.3.2 CIOU Loss
一个优秀的回归定位损失应该考虑到 3 个参数,分别是重叠面积、中心点距离和长宽比。 CIOU Loss 公式如下图所示:
其中 a 是权重, v 是用来度量长宽比的相似性,公式如下:
3.4. Focal Loss
Focal Loss 的引入主要是为了解决难易样本数量不平衡的问题,实际可以使用的范围非常广泛。 one-stage 的目标检测器通常会产生高达 100k 的候选目标,只有极少数是正样本,正负样本数量非常不平衡。我们在计算分类的时候常用的损失——交叉熵的公式如下:
为了解决正负样本不平衡的问题,我们通常会在交叉熵损失的前面加上一个参数 a ,即:
但这并不能解决全部问题。根据正、负、难、易,样本一共可以分为以下四类:正难、正易、负难、负易。
尽管 a 平衡了正负样本,但对难易样本的不平衡没有任何帮助。而实际上,目标检测中大量的候选目标都是像下图一样的易分样本。
这些样本的损失很低,但是由于数量极不平衡,易分样本的数量相对来讲太多,最终主导了总的损失。而本文的作者认为,易分样本(即置信度高的样本)对模型的提升效果非常小,模型应该主要关注与那些难分样本。这时候, Focal Loss 应运而生。一个简单的思想:把高置信度 (p) 样本的损失再降低一些不就好了吗。
举个例, r 取 2 时,如果 p=0.968 , (1-0.968)^2 ,损失衰减了 1000 倍。
Focal Loss 的最终形式结合了上面的公式 (2) 。这很好理解,公式 (3) 解决了难易样本的不平衡,公式 (2) 解决了正负样本的不平衡,将公式(2)与(3)结合使用,同时解决正负难易2个问题!
最终的 Focal Loss 形式如下:
实验表明 r 取 2 , a 取 0.25 的时候效果最佳。
3.5. Rectangular inference
Rectangular inference 矩形推理可以显著的减少推理时间,在说明 Rectangular inference 之前,先了解一下 Square Inference 。
众所周知, YOLOv3 下采样了 32 倍,因此输入网络的长宽需要是 32 的倍数,最常用的分辨率就是 416 了。Square Inference 就是输入为正方形,具体过程为:求得较长边缩放到 416 的比例,然后对图片长宽按这个比例缩放,使得较长边达到 416 再对较短边进行填充使得较短边也达到 416 。使用 Square Inference 方法明显存在大量的冗余部分,一个很自然的想法就是,能不能去掉这些填充的部分但是又要满足长宽是 32 的倍数?这就是 Rectangular Inference 的思想了,具体过程为:求得较长边缩放到 416 的比例,然后对图片 wh 按这个比例缩放,使得较长边达到 416 ,再对较短边进行尽量少的填充使得较短边满足 32 的倍数。