推荐博客

从yolov1至yolov4的进阶之路

你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (上)

你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (中)

你一定从未看过如此通俗易懂的YOLO系列(从v1到v5)模型解读 (下)

写给小白的Yolo介绍

比较流行的算法可以分为两类,一类是基于Region Proposal的R-CNN系算法(R-CNN,Fast R-CNN, Faster R-CNN),它们是two-stage的,需要先使用启发式方法(selective search)或者CNN网络(RPN)产生Region Proposal,然后再在Region Proposal上做分类与回归。

YOLO(You Only Look Once)

img

YOLO v1:直接回归出位置。

YOLO v2:全流程多尺度方法。

YOLO v3:多尺度检测头,resblock darknet53

YOLO v4:cspdarknet53,spp,panet,tricks

0 前言

本文目的是用尽量浅显易懂的语言让零基础小白能够理解什么是YOLO系列模型,以及他们的设计思想和改进思路分别是什么。我不会把YOLO的论文给你用软件翻译一遍,这样做毫无意义;也不会使用太专业晦涩的名词和表达,对于每一个新的概念都会解释得尽量通俗一些,目的是使得你能像看故事一样学习YOLO模型,我觉得这样的学习方式才是知乎博客的意义所在。

为了使本文尽量生动有趣,我用葫芦娃作为例子展示YOLO的过程(真的是尽力了。。。)。

img

葫芦娃

同时,会对YOLO v1和YOLOv5的代码进行解读,其他的版本就只介绍改进了。


1 先从一款强大的app说起

imgi detection APP

YOLO v5其实一开始是以一款app进入人们的视野的,就是上图的这个,叫:i detection(图上标的是YOLO v4,但其实算法是YOLO v5),使用iOS系列的小伙伴呢,就可以立刻点赞后关掉我这篇文章,去下载这个app玩一玩。在任何场景下(工业场景,生活场景等等)都可以试试这个app和这个算法,这个app中间还有一个button,来调节app使用的模型的大小,更大的模型实时性差但精度高,更小的模型实时性好但精度差。

值得一提的是,这款app就是YOLO v5的作者亲自完成的。而且,我写这篇文章的时候YOLO v5的论文还没有出来,还在实验中,等论文出来应该是2020年底或者2021年初了。

读到这里,你觉得YOLO v5的最大特点是什么?

答案就是:一个字:快,应用于移动端,模型小,速度快。

首先我个人觉得任何一个模型都有下面3部分组成:

  • 前向传播部分:90%
  • 损失函数部分
  • 反向传播部分

其中前向传播部分占用的时间应该在90%左右,即搞清楚前向传播部分也就搞清楚了这模型的实现流程和细节。本着这一原则,我们开始YOLO系列模型的解读:


2 不得不谈的分类模型

在进入目标检测任务之前首先得学会图像分类任务,这个任务的特点是输入一张图片,输出是它的类别。

对于输入图片,我们一般用一个矩阵表示。

对于输出结果,我们一般用一个one-hot vector表示: [公式] ,哪一维是1,就代表图片属于哪一类。

所以,在设计神经网络时,结构大致应该长这样:

img [公式] cbrp16[公式] cbrp32[公式] cbrp64[公式] cbrp128[公式][公式] fc256-fc[10]

这里的cbrp指的是conv,bn,relu,pooling的串联。

由于输入要是one-hot形式,所以最后我们设计了2个fc层(fully connencted layer),我们称之为**“分类头”或者“决策层”**。


3 YOLO系列思想的雏形:YOLO v0

有了上面的分类器,我们能不能用它来做检测呢?

要回答这个问题,首先得看看检测器和分类器的输入输出有什么不一样。首先他们的输入都是image,但是分类器的输出是一个one-hot vector,而检测器的输出是一个框(Bounding Box)。

,该怎么表示?

在一个图片里面表示一个框,有很多种方法,比如:

imgx,y,w,h表示一个框

  • x,y,w,h(如上图)
  • p1,p2,p3,p4(4个点坐标)
  • cx,cy,w,h(cx,cy为中心点坐标)
  • x,y,w,h,angle(还有的目标是有角度的,这时叫做Rotated Bounding Box)

所以表示的方法不是一成不变的,但你会发现:不管你用什么形式去表达这个Bounding Box,你模型输出的结果一定是一个vector,那这个vector和分类模型输出的vector本质上有什么区别吗?

答案是:没有,都是向量而已,只是分类模型输出是one-hot向量,检测模型输出是我们标注的结果。

所以你应该会发现,检测的方法呼之欲出了。那分类模型可以用来做检测吗?

当然可以,这时,你可以把检测的任务当做是遍历性的分类任务。

如何遍历?

我们的目标是一个个框,那就用这个框去遍历所有的位置,所有的大小。

比如下面这张图片,我需要你检测葫芦娃的脸,如图1所示:

img图1:检测葫芦娃的脸

我们可以对边框的区域进行二分类:属于头或者不属于头。

你先预设一个框的大小,然后在图片上遍历这个框,比如第一行全都不是头。第4个框只有一部分目标在,也不算。第5号框算是一个头,我们记住它的位置。这样不断地滑动,就是遍历性地分类。

接下来要遍历框的大小:因为你刚才是预设一个框的大小,但葫芦娃的头有大有小,你还得遍历框的大小,如下图2所示:

img图2:遍历框的大小

还没有结束,刚才滑窗时是挨个滑,但其实没有遍历所有的位置,更精确的遍历方法应该如下图3所示:

img

图3:更精确地遍历框的位置

这种方法其实就是RCNN全家桶的初衷,专业术语叫做:滑动窗口分类方法

现在需要你思考一个问题:这种方法的精确和什么因素有关?

答案是:遍历得彻不彻底。遍历得越精确,检测器的精度就越高。所以这也就带来一个问题就是:检测的耗时非常大

举个例子:比如输入图片大小是(800,1000)也就意味着有800000个位置。窗口大小最小 [公式] ,最大 [公式] ,所以这个遍历的次数是无限次。我们看下伪代码:

img

滑动窗口分类方法伪代码

那这种方法如何训练呢?

本质上还是训练一个二分类器。这个二分类器的输入是一个框的内容,输出是(前景/背景)

第1个问题:

框有不同的大小,对于不同大小的框,输入到相同的二分类器中吗?

是的。要先把不同大小的input归一化到统一的大小。

第2个问题:

背景图片很多,前景图片很少:二分类样本不均衡。

确实是这样,你看看一张图片有多少框对应的是背景,有多少框才是葫芦娃的头。

以上就是传统检测方法的主要思路:

  • 耗时。
  • 操作复杂,需要手动生成大量的样本。

到现在为止,我们用分类的算法设计了一个检测器,它存在着各种各样的问题,现在是优化的时候了(接下来正式进入YOLO系列方法了):

YOLO的作者当时是这么想的:你分类器输出一个one-hot vector,那我把它换成**(x,y,w,h,c)**,c表示confidence置信度,把问题转化成一个回归问题,直接回归出Bounding Box的位置不就好了吗?

刚才的分类器是:img [公式] cbrp16[公式] cbrp32[公式] cbrp64[公式] cbrp128[公式][公式] fc256-fc[10]

现在我变成:img [公式] cbrp16[公式] cbrp32[公式] cbrp64[公式] cbrp128[公式][公式] fc256-fc[5],这个输出是**(x,y,w,h,c)**,不就变成了一个检测器吗?

本质上都是矩阵映射到矩阵,只是代表的意义不一样而已。

传统的方法为什么没有这么做呢?我想肯定是效果不好,终其原因是算力不行,conv操作还没有推广。

好,现在模型是:

img [公式] cbrp16[公式] cbrp32[公式] cbrp64[公式] cbrp128[公式][公式] fc256-fc[5] [公式] c,x,y,w,h

img

那如何组织训练呢?找1000张图片,把label设置为 [公式] 。这里 [公式] 代表真值。有了数据和标签,就完成了设计。

我们会发现,这种方法比刚才的滑动窗口分类方法简单太多了。这一版的思路我把它叫做YOLO v0,因为它是You Only Look Once最简单的版本。


4 YOLO v1终于诞生

  • 需求1:YOLO v0只能输出一个目标,那比如下图4的多个目标怎么办呢?

img

图4:多个目标情况

你可能会回答:我输出N个向量不就行了吗?但具体输出多少个合适呢?图4有7个目标,那有的图片有几百个目标,你这个N又该如何调整呢?

答:为了保证所有目标都被检测到,我们应该输出尽量多的目标。

img输出尽量多的目标

但这种方法也不是最优的,最优的应该是下图这样:

img

图5:用一个(c,x,y,w,h)去负责image某个区域的目标

如图5所示:用一个(c,x,y,w,h)去负责image某个区域的目标。

比如说图片设置为16个区域,每个区域用1个(c,x,y,w,h)去负责:

img图6:图片设置为16个区域

就可以一次输出16个框,每个框是1个(c,x,y,w,h),如图6所示。

为什么这样子更优?因为conv操作是位置强相关的,就是原来的目标在哪里,你conv之后的feature map上还在哪里,所以图片划分为16个区域,结果也应该分布在16个区域上,所以我们的结果(Tensor)的维度size是:(5,4,4)

那现在你可能会问:c的真值该怎么设置呢?

**答:**看葫芦娃的大娃,他的脸跨了4个区域(grid),但只能某一个grid的c=1,其他的c=0。那么该让哪一个grid的c=1呢?就看他的脸的中心落在了哪个grid里面。根据这一原则,c的真值为下图7所示:

img图7:c的label值

但是你发现7个葫芦娃只有6个1,原因是某一个grid里面有2个目标,确实如此,第三行第三列的grid既有水娃又有隐身娃。这种一个区域有多个目标的情况我们目前没法解决,因为我们的模型现在能力就这么大,只能在一个区域中检测出一个目标,如何改进我们马上就讨论,你可以现在先自己想一想。

总之现在我们设计出了模型的输出结果,那距离完成模型的设计还差一个损失函数,那Loss咋设计呢?看下面的伪代码:

1
2
3
4
5
6
7
8
9
loss = 0
for img in img_all:
for i in range(4):
for j in range(4):
loss_ij = lamda_1*(c_pred-c_label)**2 + c_label*(x_pred-x_label)**2 +\
c_label*(y_pred-y_label)**2 + c_label*(w_pred-w_label)**2 + \
c_label*(h_pred-h_label)**2
loss += loss_ij
loss.backward()

遍历所有图片,遍历所有位置,计算loss。

  • 好现在模型设计完了,回到刚才的问题:模型现在能力就这么大,只能在一个区域中检测出一个目标,如何改进?

**答:**刚才区域是 [公式] ,现在变成 [公式] ,或者更大,使区域更密集,就可以缓解多个目标的问题,但无法从根本上去解决。

  • 另一个问题,按上面的设计你检测得到了16个框,可是图片上只有7个葫芦娃的脸,怎么从16个结果中筛选出7个我们要的呢?

答:

**法1:聚类。**聚成7类,在这7个类中,选择confidence最大的框。听起来挺好。

**法1的bug:**2个目标本身比较近聚成了1个类怎么办?如果不知道到底有几个目标呢?为何聚成7类?不是3类?

**法2:NMS(非极大值抑制)。**2个框重合度很高,大概率是一个目标,那就只取一个框。

重合度的计算方法:交并比IoU=两个框的交集面积/两个框的并集面积。

具体算法:

img

面试必考的NMS

**法1的bug:**2个目标本身比较近怎么办?依然没有解决。

如果不知道到底有几个目标呢?NMS自动解决了这个问题。

**面试的时候会问这样一个问题:**NMS的适用情况是什么?

**答:**1图多目标检测时用NMS。

到现在为止我们终于解决了第4节开始提出的多个目标的问题,现在又有了新的需求:

  • 需求2:多类的目标怎么办呢?

比如说我现在既要你检测葫芦娃的脸,又要你检测葫芦娃的葫芦,怎么设计?

img [公式] cbrp16[公式] cbrp32[公式] cbrp64[公式] cbrp128[公式][公式] fc256-fc[5+2]*N [公式] [c,x,y,w,h,one-hot]*N

2个类,one-hot就是[0,1],[1,0]这样子,如下图8所示:

img

图8:多类的目标的label

伪代码依然是:

1
2
3
4
5
6
7
8
9
10
11
12
loss = 0
for img in img_all:
for i in range(3):
for j in range(4):
c_loss = lamda_1*(c_pred-c_label)**2
geo_loss = c_label*(x_pred-x_label)**2 +\
c_label*(y_pred-y_label)**2 + c_label*(w_pred-w_label)**2 + \
c_label*(h_pred-h_label)**2
class_loss = 1/m * mse_loss(p_pred, p_label)
loss_ij =c_loss + geo_loss + class_loss
loss += loss_ij
loss.backward()

至此,多个类的问题也解决了,现在又有了新的需求:

  • 需求3:小目标检测怎么办呢?

小目标总是检测不佳,所以我们专门设计神经元去拟合小目标。

img

图9:多类的小目标的label,分别预测大目标和小目标

对于每个区域,我们用2个五元组(c,x,y,w,h),一个负责回归大目标,一个负责回归小目标,同样添加one-hot vector,one-hot就是[0,1],[1,0]这样子,来表示属于哪一类(葫芦娃的头or葫芦娃的葫芦)。

伪代码变为了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
loss = 0
for img in img_all:
for i in range(3):
for j in range(4):
c_loss = lamda_1*(c_pred-c_label)**2
geo_loss = c_label_big*(x_big_pred-x_big_label)**2 +\
c_label_big*(y_big_pred-y_big_label)**2 + c_label_big*(w_big_pred-w_big_label)**2 + \
c_label_big*(h_big_pred-h_big_label)**2 +\
c_label_small*(x_small_pred-x_small_label)**2 +\
c_label_small*(y_small_pred-y_small_label)**2 + c_label_small*(w_small_pred-w_small_label)**2 + \
c_label_small*(h_small_pred-h_small_label)**2
class_loss = 1/m * mse_loss(p_pred, p_label)
loss_ij =c_loss + geo_loss + class_loss
loss += loss_ij
loss.backward()

至此,小目标的问题也有了解决方案。

到这里,我们设计的检测器其实就是YOLO v1,只是有的参数跟它不一样,我们看论文里的图:

img

YOLO v1

img

YOLO v1

YOLO v1其实就是把我们划分的16个区域变成了 [公式] 个区域,我们预测16个目标,YOLO v1预测49个目标。我们是2类(葫芦娃的头or葫芦娃的葫芦),YOLO v1是20类。

[公式]

backbone也是一堆卷积+检测头(FC层),所以说设计到现在,我们其实是把YOLO v1给设计出来了。

再看看作者的解释:

img

发现train的时候用的小图片,检测的时候用的是大图片(肯定是经过了无数次试验证明了效果好)。

结构学完了,再看loss函数,并比较下和我们设计的loss函数有什么区别。

img

YOLO v1 loss函数

解读一下这个损失函数:

我们之前说的损失函数是设计了3个for循环,而作者为了方便写成了求和的形式:

  • 前2行计算前景的geo_loss。
  • 第3行计算前景的confidence_loss。
  • 第4行计算背景的confidence_loss。
  • 第5行计算分类损失class_loss。

伪代码上面已经有了,现在我们总体看一下这个模型:

img [公式] cbrp192[公式] cbrp256[公式] cbrp512[公式] cbrp1024[公式][公式] fc4096-fc[5+2]*N [公式] [公式]

检测层的设计:回归坐标值+one-hot分类

img

检测层的设计

  • 样本不均衡的问题解决了吗?

没有计算背景的geo_loss,只计算了前景的geo_loss,这个问题YOLO v1回避了,依然存在。

最后我们解读下YOLO v1的代码:

1.模型定义:

定义特征提取层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class VGG(nn.Module):
def __init__(self):
super(VGG,self).__init__()
# the vgg's layers
#self.features = features
cfg = [64,64,'M',128,128,'M',256,256,256,'M',512,512,512,'M',512,512,512,'M']
layers= []
batch_norm = False
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2,stride = 2)]
else:
conv2d = nn.Conv2d(in_channels,v,kernel_size=3,padding = 1)
if batch_norm:
layers += [conv2d,nn.Batchnorm2d(v),nn.ReLU(inplace=True)]
else:
layers += [conv2d,nn.ReLU(inplace=True)]
in_channels = v
# use the vgg layers to get the feature
self.features = nn.Sequential(*layers)
# 全局池化
self.avgpool = nn.AdaptiveAvgPool2d((7,7))
# 决策层:分类层
self.classifier = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,1000),
)

for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias,0)
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,1)
elif isinstance(m,nn.Linear):
nn.init.normal_(m.weight,0,0.01)
nn.init.constant_(m.bias,0)

def forward(self,x):
x = self.features(x)
x_fea = x
x = self.avgpool(x)
x_avg = x
x = x.view(x.size(0),-1)
x = self.classifier(x)
return x,x_fea,x_avg
def extractor(self,x):
x = self.features(x)
return x

定义检测头:

1
2
3
4
5
6
self.detector = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096,1470),
)

整体模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class YOLOV1(nn.Module):
def __init__(self):
super(YOLOV1,self).__init__()
vgg = VGG()
self.extractor = vgg.extractor
self.avgpool = nn.AdaptiveAvgPool2d((7,7))
# 决策层:检测层
self.detector = nn.Sequential(
nn.Linear(512*7*7,4096),
nn.ReLU(True),
nn.Dropout(),
#nn.Linear(4096,1470),
nn.Linear(4096,245),
#nn.Linear(4096,5),
)
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias,0)
elif isinstance(m,nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,1)
elif isinstance(m,nn.Linear):
nn.init.normal_(m.weight,0,0.01)
nn.init.constant_(m.bias,0)
def forward(self,x):
x = self.extractor(x)
#import pdb
#pdb.set_trace()
x = self.avgpool(x)
x = x.view(x.size(0),-1)
x = self.detector(x)
b,_ = x.shape
#x = x.view(b,7,7,30)
x = x.view(b,7,7,5)

#x = x.view(b,1,1,5)
return x

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
if __name__ == '__main__':
vgg = VGG()
x = torch.randn(1,3,512,512)
feature,x_fea,x_avg = vgg(x)
print(feature.shape)
print(x_fea.shape)
print(x_avg.shape)

yolov1 = YOLOV1()
feature = yolov1(x)
# feature_size b*7*7*30
print(feature.shape)

2.模型训练:

主函数:

1
2
if __name__ == "__main__":
train()

下面看train()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def train():
for epoch in range(epochs):
ts = time.time()
for iter, batch in enumerate(train_loader):
optimizer.zero_grad()
# 取图片
inputs = input_process(batch)
# 取标注
labels = target_process(batch)

# 获取得到输出
outputs = yolov1_model(inputs)
#import pdb
#pdb.set_trace()
#loss = criterion(outputs, labels)
loss,lm,glm,clm = lossfunc_details(outputs,labels)
loss.backward()
optimizer.step()
#print(torch.cat([outputs.detach().view(1,5),labels.view(1,5)],0).view(2,5))
if iter % 10 == 0:
# print(torch.cat([outputs.detach().view(1,5),labels.view(1,5)],0).view(2,5))
print("epoch{}, iter{}, loss: {}, lr: {}".format(epoch, iter, loss.data.item(),optimizer.state_dict()['param_groups'][0]['lr']))

#print("Finish epoch {}, time elapsed {}".format(epoch, time.time() - ts))
#print("*"*30)
#val(epoch)
scheduler.step()

训练过程比较常规,先取1个batch的训练数据,分别得到inputs和labels,依次计算loss,反传,step等。

下面说下2个训练集的数据处理函数:

input_process:

1
2
3
4
5
6
7
8
9
10
11
def input_process(batch):
#import pdb
#pdb.set_trace()
batch_size=len(batch[0])
input_batch= torch.zeros(batch_size,3,448,448)
for i in range(batch_size):
inputs_tmp = Variable(batch[0][i])
inputs_tmp1=cv2.resize(inputs_tmp.permute([1,2,0]).numpy(),(448,448))
inputs_tmp2=torch.tensor(inputs_tmp1).permute([2,0,1])
input_batch[i:i+1,:,:,:]= torch.unsqueeze(inputs_tmp2,0)
return input_batch

batch[0]为image,batch[1]为label,batch_size为1个batch的图片数量。
batch[0][i]为这个batch的第i张图片,inputs_tmp2为尺寸变成了3,448,448之后的图片,再经过unsqueeze操作拓展1维,size=[1,3,448,448],存储在input_batch中。

最后,返回的是size=[batch_size,3,448,448]的输入数据。

target_process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def target_process(batch,grid_number=7):
# batch[1]表示label
# batch[0]表示image
batch_size=len(batch[0])
target_batch= torch.zeros(batch_size,grid_number,grid_number,30)
#import pdb
#pdb.set_trace()
for i in range(batch_size):
labels = batch[1]
batch_labels = labels[i]
#import pdb
#pdb.set_trace()
number_box = len(batch_labels['boxes'])
for wi in range(grid_number):
for hi in range(grid_number):
# 遍历每个标注的框
for bi in range(number_box):
bbox=batch_labels['boxes'][bi]
_,himg,wimg = batch[0][i].numpy().shape
bbox = bbox/ torch.tensor([wimg,himg,wimg,himg])
#import pdb
#pdb.set_trace()
center_x= (bbox[0]+bbox[2])*0.5
center_y= (bbox[1]+bbox[3])*0.5
#print("[%s,%s,%s],[%s,%s,%s]"%(wi/grid_number,center_x,(wi+1)/grid_number,hi/grid_number,center_y,(hi+1)/grid_number))
if center_x<=(wi+1)/grid_number and center_x>=wi/grid_number and center_y<=(hi+1)/grid_number and center_y>= hi/grid_number:
#pdb.set_trace()
cbbox = torch.cat([torch.ones(1),bbox])
# 中心点落在grid内,
target_batch[i:i+1,wi:wi+1,hi:hi+1,:] = torch.unsqueeze(cbbox,0)
#else:
#cbbox = torch.cat([torch.zeros(1),bbox])
#import pdb
#pdb.set_trace()
#rint(target_batch[i:i+1,wi:wi+1,hi:hi+1,:])
#target_batch[i:i+1,wi:wi+1,hi:hi+1,:] = torch.unsqueeze(cbbox,0)
return target_batch

要从batch里面获得label,首先要想清楚label(就是bounding box)应该是什么size,输出的结果应该是 [公式] 的,所以label的size应该是:[batch_size,7,7,30]。在这个程序里我们实现的是输出 [公式] 。这个 [公式] 就是x,y,w,h,所以label的size应该是:[batch_size,7,7,5]
batch_labels表示这个batch的第i个图片的label,number_box表示这个图有几个真值框。
接下来3重循环遍历每个grid的每个框,bbox表示正在遍历的这个框。
bbox = bbox/ torch.tensor([wimg,himg,wimg,himg])表示对x,y,w,h进行归一化。
接下来if语句得到confidence的真值,存储在target_batch中返回。

最后是loss函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def lossfunc_details(outputs,labels):
# 判断维度
assert ( outputs.shape == labels.shape),"outputs shape[%s] not equal labels shape[%s]"%(outputs.shape,labels.shape)
#import pdb
#pdb.set_trace()
b,w,h,c = outputs.shape
loss = 0
#import pdb
#pdb.set_trace()
conf_loss_matrix = torch.zeros(b,w,h)
geo_loss_matrix = torch.zeros(b,w,h)
loss_matrix = torch.zeros(b,w,h)

for bi in range(b):
for wi in range(w):
for hi in range(h):
#import pdb
#pdb.set_trace()
# detect_vector=[confidence,x,y,w,h]
detect_vector = outputs[bi,wi,hi]
gt_dv = labels[bi,wi,hi]
conf_pred = detect_vector[0]
conf_gt = gt_dv[0]
x_pred = detect_vector[1]
x_gt = gt_dv[1]
y_pred = detect_vector[2]
y_gt = gt_dv[2]
w_pred = detect_vector[3]
w_gt = gt_dv[3]
h_pred = detect_vector[4]
h_gt = gt_dv[4]
loss_confidence = (conf_pred-conf_gt)**2
#loss_geo = (x_pred-x_gt)**2 + (y_pred-y_gt)**2 + (w_pred**0.5-w_gt**0.5)**2 + (h_pred**0.5-h_gt**0.5)**2

loss_geo = (x_pred-x_gt)**2 + (y_pred-y_gt)**2 + (w_pred-w_gt)**2 + (h_pred-h_gt)**2
loss_geo = conf_gt*loss_geo
loss_tmp = loss_confidence + 0.3*loss_geo
#print("loss[%s,%s] = %s,%s"%(wi,hi,loss_confidence.item(),loss_geo.item()))
loss += loss_tmp
conf_loss_matrix[bi,wi,hi]=loss_confidence
geo_loss_matrix[bi,wi,hi]=loss_geo
loss_matrix[bi,wi,hi]=loss_tmp
#打印出batch中每张片的位置loss,和置信度输出
print(geo_loss_matrix)
print(outputs[0,:,:,0]>0.5)
return loss,loss_matrix,geo_loss_matrix,conf_loss_matrix

首先需要注意:label和output的size应该是:[batch_size,7,7,5]。
outputs[bi,wi,hi]就是一个5位向量: [公式]
我们分别计算了loss_confidence和loss_geo,因为我们实现的这个模型只检测1个类,所以没有class_loss。

为了使本文尽量生动有趣,我仍然用葫芦娃作为例子展示YOLO的过程(真的是尽力了。。。)。

img葫芦娃

下面进入正题,首先回顾下YOLO v1的模型结构,忘记了的同学请看上面的文章并点赞,如下面2图所示:

imgYOLO

imgYOLO

我们认为,检测模型=特征提取器+检测头

在YOLO v1的模型中检测头就是最后的2个全连接层(Linear in PyTorch),它们是参数量最大的2个层,也是最值得改进的2个层。后面的YOLO模型都对这里进行改进:

YOLO v1一共预测49个目标,一共98个框。

5 YOLO v2

  • 检测头的改进:

YOLO v1虽然快,但是预测的框不准确,很多目标找不到:

  • 预测的框不准确:准确度不足。
  • 很多目标找不到:recall不足。

我们一个问题一个问题解决,首先第1个:

  • 问题1:预测的框不准确:

当时别人是怎么做的?

同时代的检测器有R-CNN,人家预测的是偏移量。

什么是偏移量?

imgYOLO v2

之前YOLO v1直接预测x,y,w,h,范围比较大,现在我们想预测一个稍微小一点的值,来增加准确度。

不得不先介绍2个新概念:基于grid的偏移量和基于anchor的偏移量。什么意思呢?

基于anchor的偏移量的意思是,anchor的位置是固定的,偏移量=目标位置-anchor的位置

基于grid的偏移量的意思是,grid的位置是固定的,偏移量=目标位置-grid的位置

Anchor是什么玩意?

Anchor是R-CNN系列的一个概念,你可以把它理解为一个预先定义好的框,它的位置,宽高都是已知的,是一个参照物,供我们预测时参考。

上面的图就是YOLO v2给出的改进,你可能现在看得一脸懵逼,我先解释下各个字母的含义:

[公式] :模型最终得到的的检测结果。

[公式] :模型要预测的值。

[公式] :grid的左上角坐标,如下图所示。

[公式] :Anchor的宽和高,这里的anchor是人为定好的一个框,宽和高是固定的。

img

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

这里还涉及到一个尺寸问题:

刚才说到 [公式] 是模型要预测的值,这里 [公式] 为grid的坐标,画个图就明白了:

img图1:原始值

如图1所示,假设此图分为9个grid,GT如红色的框所示,Anchor如紫色的框所示。图中的数字为image的真实信息。

我们首先会对这些值归一化,结果如下图2所示:

img图2:要预测的值

归一化之后你会发现,要预测的值就变为了:

[公式]

这是一个偏移量,且值很小,有利于神经网络的学习。

**你可能会有疑问:**为什么YOLO v2改预测偏移量而不是直接去预测 [公式]

上面我说了作者看到了同时代的R-CNN,人家预测的是偏移量。另一个重要的原因是:直接预测位置会导致神经网络在一开始训练时不稳定,使用偏移量会使得训练过程更加稳定,性能指标提升了5%左右。

位置上不使用Anchor框,宽高上使用Anchor框。以上就是YOLO v2的一个改进。用了YOLO v2的改进之后确实是更准确了,但别激动,上面还有一个问题呢~

  • 问题2:很多目标找不到:

你还记得上一篇讲得YOLO v1一次能检测多少个目标吗?答案是49个目标,98个框,并且2个框对应一个类别。可以是大目标也可以是小目标。因为输出的尺寸是:[N, 7, 7, 30]。式中N为图片数量,7,7为49个区域(grid)。

[公式]

YOLO v2首先把 [公式] 个区域改为 [公式] 个区域,每个区域有5个anchor,且每个anchor对应着1个类别,那么,输出的尺寸就应该为:[N,13,13,125]。

[公式]

这里面有个bug,就是YOLO v2先对每个区域得到了5个anchor作为参考,那你就会问2个问题:

1.为什么要用Anchor呢?

答:一开始YOLO v1的初始训练过程很不稳定,在YOLO v2中,作者观察了很多图片的所有Ground Truth,发现:比如车,GT都是矮胖的长方形,再比如行人,GT都是瘦高的长方形,且宽高比具有相似性。那**能不能根据这一点,从数据集中预先准备几个几率比较大的bounding box,再以它们为基准进行预测呢?**这就是Anchor的初衷。

2.每个区域的5个anchor是如何得到的?

下图可以回答你的问题:

imgmethods to get the 5 anchor

方法:对于任意一个数据集,就比如说COCO吧(紫色的anchor),先对训练集的GT bounding box进行聚类,聚成几类呢?作者进行了实验之后发现5类的recall vs. complexity比较好,现在聚成了5类,当然9类的mAP最好,预测的最全面,但是在复杂度上升很多的同时对模型的准确度提升不大,所以采用了一个比较折中的办法选取了5个聚类簇,即使用5个先验框。

所以到现在为止,有了anchor再结合刚才的 [公式] ,就可以求出目标位置。

anchor是从数据集中统计得到的(Faster-RCNN中的Anchor的宽高和大小是手动挑选的)。

  • 损失函数为:

imgYOLO v2损失函数

这里的W=13,H=13,A=5。
每个 [公式] 都是一个权重值。c表示类别,r表示rectangle,即(x,y,w,h)。
第1,4行是confidence_loss,注意这里的真值变成了0和IoU(GT, anchor)的值,你看看这些细节。。。
第5行是class_loss。
第2,3行:t是迭代次数,即前12800步我们计算这个损失,后面不计算了。这部分意义何在?
意思是:前12800步我们会优化预测的(x,y,w,h)与anchor的(x,y,w,h)的距离+预测的(x,y,w,h)与GT的(x,y,w,h)的距离,12800步之后就只优化预测的(x,y,w,h)与GT的(x,y,w,h)的距离,为啥?因为这时的预测结果已经较为准确了,anchor已经满足我了我们了,而在一开始预测不准的时候,用上anchor可以加速训练。
你看看这操作多么的细节。。。
[公式] 是什么?第k个anchor与所有GT的IoU的maximum,如果大于一个阈值,就 [公式] ,否则的话: [公式]

好,到现在为止,YOLO v2做了这么多改进,整体性能大幅度提高,但是小目标检测仍然是YOLO v2的痛。直到kaiming大神的ResNet出现,backbone可以更深了,所以darknet53诞生。

最后我们做个比较:

imgYOLO v1和v2的比较


6 YOLO v3

  • 检测头的改进:

之前在说小目标检测仍然是YOLO v2的痛,YOLO v3是如何改进的呢?如下图所示。

imgYOLO v3

我们知道,YOLO v2的检测头已经由YOLO v1的 [公式] 变为 [公式]了,我们看YOLO v3检测头分叉了,分成了3部分:

  • 13133*(4+1+80)
  • 26263*(4+1+80)
  • 52523*(4+1+80)

预测的框更多更全面了,并且分级了。

我们发现3个分支分别为32倍下采样,16倍下采样,8倍下采样,分别取预测大,中,小目标。为什么这样子安排呢?

因为32倍下采样每个点感受野更大,所以去预测大目标,8倍下采样每个点感受野最小,所以去预测小目标。专人专事。

发现预测地更准确了,性能又提升了。

又有人会问,你现在是3个分支,我改成5个,6个分支会不会更好?

理论上会,但还是那句话,作者遵循recall vs. complexity的trade off。

图中的123456789是什么意思?

答:框。每个grid设置9个先验框,3个大的,3个中的,3个小的。

每个分支预测3个框,每个框预测5元组+80个one-hot vector类别,所以一共size是:

3*(4+1+80)

每个分支的输出size为:

  • [13,13,3*(4+1+80)]
  • [26,26,3*(4+1+80)]
  • [52,52,3*(4+1+80)]

当然你也可以用5个先验框,这时每个分支的输出size为:

  • [13,13,5*(4+1+80)]
  • [26,26,5*(4+1+80)]
  • [52,52,5*(4+1+80)]

读到这里,请你数一下YOLO v3可以预测多少个bounding box?

[公式]

多了这么多可以预测的bounding box,模型的能力增强了。

为确定priors,YOLO v3 应用k均值聚类。然后它预先选择9个聚类簇

对于COCO数据集,锚定框的宽度和高为**(10 ×13),(16 ×30),(33 ×23),(30 ×61),(62 ×45),(59 ×119),(116 × 90),( 156 ×198),(373373×326326)**。这应该是按照输入图像的尺寸416×416 计算得到的。这 9个priors根据它们的尺度分为3个不同的组。在检测目标时,给一特定的特征图分配一个组。

就对应了下面这个图:

imgYOLO v3

检测头是DBL,定义在图上,没有了FC。

还有一种画法,更加直观一点:

img

imgYOLO v3 head

anchor和YOLO v2一样,依然是从数据集中统计得到的。

  • 损失函数为:

imgYOLO v3损失函数

第4行说明:loss分3部分组成:
第1行代表geo_loss,S代表13,26,52,就是grid是几乘几的。B=5。
第2行代表confidence_loss,和YOLO v2一模一样。
第3行代表class_loss,和YOLO v2的区别是改成了交叉熵。

  • 边界框预测和代价函数的计算:

YOLO v3使用多标签分类,用多个独立的logistic分类器代替softmax函数,以计算输入属于特定标签的可能性。在计算分类损失进行训练时,YOLO v3对每个标签使用二元交叉熵损失。

正负样本的确定:

如果边界框先验(锚定框)与 GT 目标比其他目标重叠多,则相应的目标性得分应为 1
对于重叠大于等于0.5的其他先验框(anchor),忽略,不算损失
每个 GT 目标仅与一个先验边界框相关联。 如果没有分配先验边界框,则不会导致分类和定位损失,只会有目标性的置信度损失。
使用**tx和ty(而不是 bx 和by)**来计算损失。

img使用tx和ty(而不是 bx 和by)来计算损失

img交叉熵损失

总结起来就是下面4句话:

  • 正样本:与GT的IOU最大的框。
  • 负样本:与GT的IOU<0.5 的框。
  • 忽略的样本:与GT的IOU>0.5 但不是最大的框。
  • 使用 tx 和ty (而不是 bx 和by )来计算损失。

最后我们做个比较:

imgYOLO v1 v2和v3的比较


7 疫情都挡不住的YOLO v4

第一次看到YOLO v4公众号发文是在疫情期间,那时候还来不了学校。不得不说疫情也挡不住作者科研的动力。

  • 检测头的改进:

YOLO v4的作者换成了Alexey Bochkovskiy大神,检测头总的来说还是多尺度的,3个尺度,分别负责大中小目标。只不过多了一些细节的改进:

1.Using multi-anchors for single ground truth

之前的YOLO v3是1个anchor负责一个GT,YOLO v4中用多个anchor去负责一个GT。方法是:对于 [公式] 来说,只要 [公式] ,就让 [公式] 去负责 [公式]

这就相当于你anchor框的数量没变,但是选择的正样本的比例增加了,就缓解了正负样本不均衡的问题

2.Eliminate_grid sensitivity

还记得之前的YOLO v2的这幅图吗?YOLO v2,YOLO v3都是预测4个这样的偏移量

img图3:YOLO v2,YOLO v3要预测的值

这里其实还隐藏着一个问题:

模型预测的结果是: [公式] ,那么最终的结果是: [公式] 。这个 [公式] 按理说应该能取到一个grid里面的任意位置。但是实际上边界的位置是取不到的,因为sigmoid函数的值域是: [公式] ,它不是 [公式] 。所以作者提出的Eliminate_grid sensitivity的意思是:将 [公式] 的计算公式改为:

[公式]

[公式]

这里的1.1就是一个示例,你也可以是1.05,1.2等等,反正要乘上一个略大于1的数,作者发现经过这样的改动以后效果会再次提升。

3.CIoU-loss

之前的YOLO v2,YOLO v3在计算geo_loss时都是用的MSE Loss,之后人们开始使用IoU Loss。

[公式] 它可以反映预测检测框与真实检测框的检测效果。

但是问题也很多:不能反映两者的距离大小(重合度)。同时因为loss=0,**当GT和bounding box不挨着时,没有梯度回传,无法进行学习训练。**如下图4所示,三种情况IoU都相等,但看得出来他们的重合度是不一样的,左边的图回归的效果最好,右边的最差:

img图4:IoU Loss不能反映两者的距离大小

  • 所以接下来的改进是:

[公式] , [公式] 为同时包含了预测框和真实框的最小框的面积。

img图4:GIoU Loss

GIoU Loss可以解决上面IoU Loss对距离不敏感的问题。但是GIoU Loss存在训练过程中发散等问题。

  • 接下来的改进是:

[公式]

其中, [公式][公式] 分别代表了预测框和真实框的中心点,且[公式]代表的是计算两个中心点间的欧式距离。[公式]代表的是能够同时包含预测框和真实框的最小闭包区域的对角线距离。

img图5:DIoU Loss

DIoU loss可以直接最小化两个目标框的距离,因此比GIoU loss收敛快得多。

DIoU loss除了这一点之外,还有一个好处是:

imgIoU Loss和GIoU loss都一样时

如上图所示,此3种情况IoU Loss和GIoU loss都一样,但是DIoU Loss右图最小,中间图次之,左图最大。

这里就是一道面试题:请总结DIoU loss的好处是?

答:

  • 收敛快(需要的epochs少)。
  • 缓解了Bounding box全包含GT问题。

但是DIoU loss只是缓解了Bounding box全包含GT问题依然没有彻底解决包含的问题,即:

img

这2种情况[公式][公式]是重合的,DIoU loss的第3项没有区别,所以在这个意义上DIoU loss依然存在问题。

  • 接下来的改进是:

惩罚项如下面公式:

[公式] 其中 [公式] 是权重函数,

[公式] 用来度量长宽比的相似性,定义为[公式]

完整的 CIoU 损失函数定义:

[公式]

最后,CIoU loss的梯度类似于DIoU loss,但还要考虑 [公式] 的梯度。在长宽在 [公式] 的情况下, [公式] 的值通常很小,会导致梯度爆炸,因此在 [公式] 实现时将替换成1。

CIoU loss实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def bbox_overlaps_ciou(bboxes1, bboxes2):
rows = bboxes1.shape[0]
cols = bboxes2.shape[0]
cious = torch.zeros((rows, cols))
if rows * cols == 0:
return cious
exchange = False
if bboxes1.shape[0] > bboxes2.shape[0]:
bboxes1, bboxes2 = bboxes2, bboxes1
cious = torch.zeros((cols, rows))
exchange = True

w1 = bboxes1[:, 2] - bboxes1[:, 0]
h1 = bboxes1[:, 3] - bboxes1[:, 1]
w2 = bboxes2[:, 2] - bboxes2[:, 0]
h2 = bboxes2[:, 3] - bboxes2[:, 1]

area1 = w1 * h1
area2 = w2 * h2

center_x1 = (bboxes1[:, 2] + bboxes1[:, 0]) / 2
center_y1 = (bboxes1[:, 3] + bboxes1[:, 1]) / 2
center_x2 = (bboxes2[:, 2] + bboxes2[:, 0]) / 2
center_y2 = (bboxes2[:, 3] + bboxes2[:, 1]) / 2

inter_max_xy = torch.min(bboxes1[:, 2:],bboxes2[:, 2:])
inter_min_xy = torch.max(bboxes1[:, :2],bboxes2[:, :2])
out_max_xy = torch.max(bboxes1[:, 2:],bboxes2[:, 2:])
out_min_xy = torch.min(bboxes1[:, :2],bboxes2[:, :2])

inter = torch.clamp((inter_max_xy - inter_min_xy), min=0)
inter_area = inter[:, 0] * inter[:, 1]
inter_diag = (center_x2 - center_x1)**2 + (center_y2 - center_y1)**2
outer = torch.clamp((out_max_xy - out_min_xy), min=0)
outer_diag = (outer[:, 0] ** 2) + (outer[:, 1] ** 2)
union = area1+area2-inter_area
u = (inter_diag) / outer_diag
iou = inter_area / union
with torch.no_grad():
arctan = torch.atan(w2 / h2) - torch.atan(w1 / h1)
v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(w2 / h2) - torch.atan(w1 / h1)), 2)
S = 1 - iou
alpha = v / (S + v)
w_temp = 2 * w1
ar = (8 / (math.pi ** 2)) * arctan * ((w1 - w_temp) * h1)
cious = iou - (u + alpha * ar)
cious = torch.clamp(cious,min=-1.0,max = 1.0)
if exchange:
cious = cious.T
return cious

所以最终的演化过程是:

MSE Loss [公式] IoU Loss[公式] GIoU Loss[公式] DIoU Loss[公式] CIoU Loss

  • YOLO v4损失函数:

所以基于上面的改进,YOLO v4的损失函数为:

imgYOLO v4的损失函数


8 代码比论文都早的YOLO v5

  • 检测头的改进:

head部分没有任何改动,和yolov3和yolov4完全相同,也是三个输出头,stride分别是8,16,32,大输出特征图检测小物体,小输出特征图检测大物体。

但采用了自适应anchor,而且这个功能还可以手动打开/关掉,具体是什么意思呢?

加上了自适应anchor的功能,个人感觉YOLO v5其实变成了2阶段方法。

先回顾下之前的检测器得到anchor的方法:

Yolo v2 v3 v4:聚类得到anchor,不是完全基于anchor的,w,h是基于anchor的,而x,y是基于grid的坐标,所以人家叫location prediction

R-CNN系列:手动指定anchor的位置。

基于anchor的方法是怎么用的:

imganchor是怎么用的

有了anchor的 [公式] ,和我们预测的偏移量 [公式] ,就可以计算出最终的output: [公式]

之前anchor是固定的,自适应anchor利用网络的学习功能,让 [公式] 也是可以学习的。我个人觉得自适应anchor策略,影响应该不是很大,除非是刚开始设置的anchor是随意设置的,一般我们都会基于实际项目数据重新运用kmean算法聚类得到anchor,这一步本身就不能少。

最后总结一下:

imgYOLO series的比较

本文只介绍了YOLO v2 v3 v4 v5对于检测头head损失函数loss的优化,剩下的backbone方面和输入端的优化实在是写不动了,放到下一篇吧。

下面进入正题,目标检测器模型的结构如下图1所示,之前看过了YOLO v2 v3 v4 v5对于检测头和loss函数的改进,如下图2所示,下面着重介绍backbone的改进:

img图1:检测器的结构

img图2:YOLO系列比较

我们发现YOLO v1只是把最后的特征分成了 [公式] 个grid,到了YOLO v2就变成了 [公式] 个grid,再到YOLO v3 v4 v5就变成了多尺度的**(strides=8,16,32),更加复杂了。那为什么一代比一代检测头更加复杂呢?答案是:因为它们的提特征网络更加强大了,能够支撑起检测头做更加复杂的操作。**换句话说,如果没有backbone方面的优化,你即使用这么复杂的检测头,可能性能还会更弱。所以引出了今天的话题:

  • backbone的改进:

YOLO v1

我们先看看YOLO v1的backbone长什么样子:

imgYOLO v1 backbone

最后2层是全连接层,其他使用了大量的卷积层,网络逐渐变宽,是非常标准化的操作。注意这里面试官可能会问你一个问题:为什么都是卷积,图上要分开画出来,不写在一起?答案是:按照feature map的分辨率画出来。分辨率A变化到分辨率B的所有卷积画在了一起。因为写代码时经常会这么做,所以问这个问题的意图是看看你是否经常写代码。

然后我们看下检测类网络的结构,如下图3所示,这个图是YOLO v4中总结的:

img图3:检测类网络的结构

YOLO v1没有Neck,Backbone是GoogLeNet,属于Dense Prediction。1阶段的检测器属于Dense Prediction,而2阶段的检测器既有Dense Prediction,又有Sparse Prediction


YOLO v2

为了进一步提升性能,YOLO v2重新训练了一个darknet-19,如下图4所示:

img图4:darknet-19

仔细观察上面的backbone的结构(双横线上方),提出3个问题:

  • 为什么没有 [公式] 卷积了?只剩下了 [公式] 卷积和 [公式] 卷积了?

**答:**vgg net论文得到一个结论,[公式] 卷积可以用更小的卷积代替,且[公式] 卷积更加节约参数,使模型更小。

网络可以做得更深,更好地提取到特征。为什么?因为每做一次卷积,后面都会接一个非线性的激活函数,更深意味着非线性的能力更强了。所以,你可能以后再也见不到 [公式] 卷积了。

另外还用了bottleneck结构(红色框):

[公式] 卷积负责扩大感受野, [公式] 卷积负责减少参数量。

  • 为什么没有FC层了?

**答:**使用了GAP(Global Average Pooling)层,把 [公式] 映射为 [公式] ,满足了输入不同尺度的image的需求。你不管输入图片是 [公式] 还是 [公式] ,最后都给你映射为 [公式]

这对提高检测器的性能有什么作用呢?

对于小目标的检测,之前输入图片是固定的大小的,小目标很难被检测准确;现在允许多尺度输入图片了,只要把图片放大,小目标就变成了大目标,提高检测的精度。

  • 为什么最后一层是softmax?

**答:**因为backbone网络darknet-19是单独train的,是按照分类网络去train的,用的数据集是imagenet,是1000个classes,所以最后加了一个softmax层,使用cross entropy loss。

接下来总结下YOLO v2的网络结构:

  • 图4中的双横线的上半部分(第0-22层)为backbone,train的方法如上文。
  • 后面的结构如下图5所示:

img图5:YOLO v2网络结构

  • 从第23层开始为检测头,其实是3个 3 * 3 * 1024 的卷积层。
  • 同时增加了一个 passthrough 层(27层),最后使用 1 * 1 卷积层输出预测结果,输出结果的size为 [公式]
  • route层的作用是进行层的合并(concat),后面的数字指的是合并谁和谁。
  • passthrough 层可以把 [公式]

YOLO2 的训练主要包括三个阶段:

  1. 先在 ImageNet 分类数据集预训练 Darknet-19,此时模型输入为 224 * 224 ,共训练 160 个 epochs。(为什么可以这样训练?因为有GAP)
  2. 将网络的输入调整为 448 * 448(注意在测试的时候使用 416 * 416 大小) ,继续在 ImageNet 数据集上 finetune 分类模型,训练 10 个 epochs。注意为什么测试的时候使用大小?答案是:将输入图像大小从448 ×448 更改为 416 ×416 。这将创建奇数空间维度(7×7 v.s 8 ×8 grid cell)。 图片的中心通常被大目标占据。 对于奇数网格单元,可以更容易确定目标所属的位置。对于一些大目标,它们中心点往落入图片中心位置,此时使用特征图中心的1个cell去预测这些目标的bounding box相对容易些,否则就要用中间4个Cells来进行预测。
  3. 修改 Darknet-19 分类模型为检测模型为图5形态,即:移除最后一个卷积层、global avgpooling 层以及 softmax 层,并且新增了3个 3 * 3 * 1024 卷积层,同时增加了一个 passthrough 层,最后使用 1 * 1 卷积层输出预测结果,并在检测数据集上继续finetune 网络。
  4. YOLO v1的训练:先使用224 * 224的分辨率训练分类网络,再切换到448 * 448的分辨率训练检测网络。而YOLO v2在使用224 * 224的分辨率训练分类网络160 epochs之后,先使用448 * 448的分辨率finetune分类网络10 epochs,再使用448 * 448的分辨率训练检测网络。可提升4%mAP。**为什么可以改变输入维度了?**因为YOLO v2中使用了GAP(Global Average Pooling)不论输入图片多大最后都给你从 [公式] ,进行分类训练。

注意这里图5有个地方得解释一下:第25层把第16层进行reorg,即passthrough操作,得到的结果为27层,再与第24层进行route,即concat操作,得到第28层。

可视化的图为:

img图5:YOLO v2可视化结构

YOLO v3

先看下YOLO v3的backbone,如下图6所示:

img图6:YOLO v3 backbone

先声明下darknet 53指的是convolution层有52层+1个conv层把1024个channel调整为1000个,你会发现YOLO v2中使用的GAP层在YOLO v3中还在用,他还是在ImageNet上先train的backbone,

观察发现依然是有bottleneck的结构和残差网络

为什么YOLO v3敢用3个检测头**?因为**他的backbone更强大了。

为什么更强大了**?因为**当时已经出现了ResNet结构。

所以YOLO v3的提高,有一部分功劳应该给ResNet。

再观察发现YOLO v3没有Pooling layer了,用的是conv(stride = 2)进行下采样,为什么?

因为Pooling layer,不管是MaxPooling还是Average Pooling,本质上都是下采样减少计算量,本质上就是不更新参数的conv,但是他们会损失信息,所以用的是conv(stride = 2)进行下采样。

下图7是YOLO v3的网络结构:

img图7:YOLO v3 Structure

img图8:YOLO v3 Structure

特征融合的方式更加直接,没有YOLO v2的passthrough操作,直接上采样之后concat在一起。


YOLO v4

图9,10展示了YOLO v4的结构:

img图9:YOLO v4 Structure

img图10:YOLO v4 Structure

img图11:YOLO v4 Structure

Yolov4的结构图和Yolov3相比,因为多了CSP结构,PAN结构,如果单纯看可视化流程图,会觉得很绕,不过在绘制出上面的图形后,会觉得豁然开朗,其实整体架构和Yolov3是相同的,不过使用各种新的算法思想对各个子结构都进行了改进。

YOLOv4使用了CSPDarknet53作为backbone,加上SPP模块,PANET作为neck,以及YOLO v3的head。

Yolov4的五个基本组件

  1. **CBM:**Yolov4网络结构中的最小组件,由Conv+Bn+Mish激活函数三者组成。
  2. **CBL:**由Conv+Bn+Leaky_relu激活函数三者组成。
  3. **Res unit:**借鉴Resnet网络中的残差结构,让网络可以构建的更深。
  4. **CSPX:**借鉴CSPNet网络结构,由三个卷积层和X个Res unint模块Concate组成。
  5. **SPP:**采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合。

其他基础操作:

  1. **Concat:**张量拼接,维度会扩充,和Yolov3中的解释一样,对应于cfg文件中的route操作。
  2. **add:**张量相加,不会扩充维度,对应于cfg文件中的shortcut操作。

Backbone中卷积层的数量:

和Yolov3一样,再来数一下Backbone里面的卷积层数量。

每个CSPX中包含3+2X个卷积层,因此整个主干网络Backbone中一共包含2+(3+21)+2+(3+22)+2+(3+28)+2+(3+28)+2+(3+24)+1=72。

  • 输入端的改进:

YOLO v4对输入端进行了改进,主要包括数据增强Mosaic、cmBN、SAT自对抗训练,使得在卡不是很多时也能取得不错的结果。

这里介绍下数据增强Mosaic:

imgMosaic数据增强

CutMix只使用了两张图片进行拼接,而Mosaic数据增强则采用了4张图片,随机缩放、随机裁剪、随机排布的方式进行拼接。

Yolov4的作者采用了Mosaic数据增强的方式。

主要有几个优点:

  1. 丰富数据集:随机使用4张图片,随机缩放,再随机分布进行拼接,大大丰富了检测数据集,特别是随机缩放增加了很多小目标,让网络的鲁棒性更好。
  2. **减少GPU:**可能会有人说,随机缩放,普通的数据增强也可以做,但作者考虑到很多人可能只有一个GPU,因此Mosaic增强训练时,可以直接计算4张图片的数据,使得Mini-batch大小并不需要很大,一个GPU就可以达到比较好的效果。

cmBN的方法如下图:

imgcmBN的方法


YOLO v5

图12,13展示了YOLO v5的结构:

img图12:YOLO v5 Structure

img图13:YOLO v5 Structure

检测头的结构基本上是一样的,融合方法也是一样。

Yolov5的基本组件

  1. **Focus:**基本上就是YOLO v2的passthrough。
  2. **CBL:**由Conv+Bn+Leaky_relu激活函数三者组成。
  3. **CSP1_X:**借鉴CSPNet网络结构,由三个卷积层和X个Res unint模块Concate组成。
  4. **CSP2_X:**不再用Res unint模块,而是改为CBL。
  5. **SPP:**采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合,如图13所示。

提特征的网络变短了,速度更快。YOLO v5的结构没有定下来,作者的代码还在持续更新。

YOLO v5的四种结构的深度:

下图展示了YOLO v5的四种结构:

img图14:YOLO四种结构深度

Yolov5代码中,每个网络结构的两个参数:

(1)Yolov5s.yaml

1
2
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50 # layer channel multiple

(2)Yolov5m.yaml

1
2
depth_multiple: 0.67  # model depth multiple
width_multiple: 0.75 # layer channel multiple

(3)Yolov5l.yaml

1
2
depth_multiple: 1.0  # model depth multiple
width_multiple: 1.0 # layer channel multiple

(4)Yolov5x.yaml

1
2
depth_multiple: 1.33  # model depth multiple
width_multiple: 1.25 # layer channel multiple

四种结构就是通过上面的两个参数,来进行控制网络的深度宽度。其中depth_multiple控制网络的深度width_multiple控制网络的宽度

Yolov5网络结构

四种结构的yaml文件中,下方的网络架构代码都是一样的。

将Backbone部分提取出来,讲解如何控制网络的宽度和深度,yaml文件中的Head部分也是同样的原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# YOLOv5 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Focus, [64, 3]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, BottleneckCSP, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 9, BottleneckCSP, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, BottleneckCSP, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 1, SPP, [1024, [5, 9, 13]]],
[-1, 3, BottleneckCSP, [1024, False]], # 9
]

在对网络结构进行解析时,yolo.py中下方的这一行代码将四种结构的depth_multiplewidth_multiple提取出,赋值给gd,gw。后面主要对这gd,gw这两个参数进行讲解。

1
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']

下面再细致的剖析下,看是如何控制每种结构,深度和宽度的。

1) 不同网络的深度

在上图中有2种结构:CSP1和CSP2,其中CSP1结构主要应用于Backbone中,CSP2结构主要应用于Neck中。

需要注意的是,四种网络结构中每个CSP结构的深度都是不同的。

a.以yolov5s为例,第一个CSP1中,使用了1个残差组件,因此是CSP1_1。而在Yolov5m中,则增加了网络的深度,在第一个CSP1中,使用了2个残差组件,因此是CSP1_2

而Yolov5l中,同样的位置,则使用了3个残差组件,Yolov5x中,使用了4个残差组件

其余的第二个CSP1和第三个CSP1也是同样的原理。

b.在第二种CSP2结构中也是同样的方式,以第一个CSP2结构为例,Yolov5s组件中使用了1组卷积,因此是CSP2_1

而Yolov5m中使用了2组,Yolov5l中使用了3组,Yolov5x中使用了4组。

其他的四个CSP2结构,也是同理。

Yolov5中,网络的不断加深,也在不断增加网络特征提取特征融合的能力。

2) 控制深度的代码

控制四种网络结构的核心代码是yolo.py中下面的代码,存在两个变量,n和gd

我们再将n和gd带入计算,看每种网络的变化结果。

1
n = max(round(n * gd), 1) if n > 1 else n  # depth gain

3) 验证控制深度的有效性

我们选择最小的yolov5s.yaml和中间的yolov5l.yaml两个网络结构,将**gd(height_multiple)**系数带入,看是否正确。

img

a. yolov5x.yaml

其中height_multiple=0.33,即gd=0.33,而n则由上面红色框中的信息获得。

以上面网络框图中的第一个CSP1为例,即上面的第一个红色框。n等于第二个数值3。

gd=0.33,带入(2)中的计算代码,结果n=1。因此第一个CSP1结构内只有1个残差组件,即CSP1_1。

第二个CSP1结构中,n等于第二个数值9,而gd=0.33,带入(2)中计算,结果n=3,因此第二个CSP1结构中有3个残差组件,即CSP1_3。

第三个CSP1结构也是同理,这里不多说。

b. yolov5l.xml

其中height_multiple=1,即gd=1

和上面的计算方式相同,第一个CSP1结构中,n=1,带入代码中,结果n=3,因此为CSP1_3。

下面第二个CSP1和第三个CSP1结构都是同样的原理。

YOLO v5的四种结构的宽度:

img图15:YOLO四种结构宽度

1) 不同网络的宽度:

如上图表格中所示,四种yolov5结构在不同阶段的卷积核的数量都是不一样的,因此也直接影响卷积后特征图的第三维度,即宽度

a.以Yolo v5s结构为例,第一个Focus结构中,最后卷积操作时,卷积核的数量是32个,因此经过Focus结构,特征图的大小变成304*304*32

而Yolo v5m的Focus结构中的卷积操作使用了48个卷积核,因此Focus结构后的特征图变成304*304*48。yolov5l,yolov5x也是同样的原理。

b. 第二个卷积操作时,Yolo v5s使用了64个卷积核,因此得到的特征图是152*152*64。而yolov5m使用96个特征图,因此得到的特征图是152*152*96。Yolo v5l,Yolo v5x也是同理。

c. 后面三个卷积下采样操作也是同样的原理。

四种不同结构的卷积核的数量不同,这也直接影响网络中,比如CSP1,CSP2等结构,以及各个普通卷积,卷积操作时的卷积核数量也同步在调整,影响整体网络的计算量。

当然卷积核的数量越多,特征图的厚度,即宽度越宽,网络提取特征的学习能力也越强

  1. 控制宽度的代码

在Yolo v5的代码中,控制宽度的核心代码是yolo.py文件里面的这一行:

1
c2 = make_divisible(c2 * gw, 8) if c2 != no else c2

它所调用的子函数make_divisible是一个很出名的函数,在各大任务中均会使用,作用是:

**make_divisible(A,B):**找到比A大的,能整除B的最小整数。

make_divisible(54,8) = 56

3) 验证控制宽度的有效性

我们还是选择最小的Yolo v5s中间的Yolo v5l两个网络结构,将width_multiple系数带入,看是否正确。

img图16:backbone

a. Yolo v5x.yaml

其中width_multiple=0.5,即gw=0.5

以第一个卷积下采样为例,即Focus结构中下面的卷积操作。

按照上面Backbone的信息,我们知道Focus中,标准的c2=64,而gw=0.5,代入(2)中的计算公式,最后的结果=32。即Yolo v5s的Focus结构中,卷积下采样操作的卷积核数量为32个。

再计算后面的第二个卷积下采样操作,标准c2的值=128,gw=0.5,代入(2)中公式,最后的结果=64,也是正确的。

b. Yolo v5l.yaml

其中width_multiple=1,即gw=1,而标准的c2=64,代入上面(2)的计算公式中,可以得到Yolo v5l的Focus结构中,卷积下采样操作的卷积核的数量为64个,而第二个卷积下采样的卷积核数量是128个。

另外的三个卷积下采样操作,以及Yolo v5m,Yolo v5x结构也是同样的计算方式。

比如:YOLO V5s默认depth_multiple=0.33, width_multiple=0.50。即BottleneckCSP中Bottleneck的数量为默认的1/3,而所有卷积操作的卷积核个数均为默认的1/2。

img

img图17:SPP结构

Focus操作如下图所示:

img图18:Focus操作

Focusslice操作如下图所示:

img图19:slice操作

这个其实就是Yolo v2里面的ReOrg+Conv操作,也是亚像素卷积的反向操作版本,简单来说就是把数据切分为4份,每份数据都是相当于2倍下采样得到的,然后在channel维度进行拼接,最后进行卷积操作。其最大好处是可以最大程度的减少信息损失而进行下采样操作

YOLO v5s默认3x640x640的输入,复制四份,然后通过切片操作将这个四个图片切成了四个3x320x320的切片,接下来使用concat从深度上连接这四个切片,输出为12x320x320,之后再通过卷积核数为32的卷积层,生成32x320x320的输出,最后经过batch_borm 和leaky_relu将结果输入到下一个卷积层。

Focus的代码如下:

1
2
3
4
5
6
7
8
class Focus(nn.Module):
# Focus wh information into c-space
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Focus, self).__init__()
self.conv = Conv(c1 * 4, c2, k, s, p, g, act)

def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2)
return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

这里解释以下PANET结构是什么意思,PAN结构来自论文Path Aggregation Network,可视化结果如图19所示:

img图20:PAN结构

可以看到包含了自底向上和自顶向下的连接,值得注意的是这里的红色虚线和绿色虚线:

FPN的结构把浅层特征传递给顶层要经历几十甚至上百层,显然经过这么多层的传递,浅层信息(小目标)丢失比较厉害。这里的红色虚线就象征着ResNet的几十甚至上百层。

自下而上的路径不到10层组成,浅层特征经过FPN的laterial connection连接到 [公式] ,再经过bottom-up path augmentation连接到顶层,经过的层数不到10层,能较好地保留浅层的信息。这里的绿色虚线就象征着自下而上的路径的不到10层。

YOLO V5借鉴了YOLO V4的修改版PANET结构。

PANET通常使用自适应特征池将相邻层在一起,以进行掩模预测。但是,当在YOLO v4中使用PANET时,此方法略麻烦,因此,YOLO v4的作者没有使用自适应特征池添加相邻层,而是对其进行Concat操作,从而提高了预测的准确性。

img图21:Modified PAN

  • 输入端的改进:

1.Mosaic数据增强,和YOLO v4一样。

2.自适应锚框计算:

在Yolo算法中,针对不同的数据集,都会有初始设定长宽的锚框

在网络训练中,网络在初始锚框的基础上输出预测框,进而和真实框groundtruth进行比对,计算两者差距,再反向更新,迭代网络参数

因此初始锚框也是比较重要的一部分,比如Yolov5在Coco数据集上初始设定的锚框:

imgYolov5在Coco数据集上初始设定的锚框

在Yolov3、Yolov4中,训练不同的数据集时,计算初始锚框的值是通过单独的程序运行的。

但Yolov5中将此功能嵌入到代码中,每次训练时,自适应的计算不同训练集中的最佳锚框值。

当然,如果觉得计算的锚框效果不是很好,也可以在代码中将自动计算锚框功能关闭

1
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')

设置成False,每次训练时,不会自动计算。

3.自适应图片缩放

在常用的目标检测算法中,不同的图片长宽都不相同,因此常用的方式是将原始图片统一缩放到一个标准尺寸,再送入检测网络中。

比如Yolo算法中常用416*416,608*608等尺寸,比如对下面800*600的图像进行缩放:,如图15所示:

img图22:Yolo算法的缩放填充

Yolov5代码中对此进行了改进,也是Yolov5推理速度能够很快的一个不错的trick。

作者认为,在项目实际使用时,很多图片的长宽比不同,因此缩放填充后,两端的黑边大小都不同,而如果填充的比较多,则存在信息冗余,影响推理速度

因此在Yolov5的代码中datasets.py的letterbox函数中进行了修改,对原始图像自适应的添加最少的黑边,如图16所示:

img图22:YOLO v5的自适应填充

图像高度上两端的黑边变少了,在推理时,计算量也会减少,即目标检测速度会得到提升。

通过这种简单的改进,推理速度得到了37%的提升,可以说效果很明显。

Yolov5中填充的是灰色,即(114,114,114),都是一样的效果,且训练时没有采用缩减黑边的方式,还是采用传统填充的方式,即缩放到416*416大小。只是在测试,使用模型推理时,才采用缩减黑边的方式,提高目标检测,推理的速度。

关于样本:

yolov5的很大区别就是在于正样本区域的定义。在yolov3中,每一个 [公式] **只有1个对应的anchor,**设定的规则是取 [公式] 最大的anchor。而且某个 [公式] 一定不可能在三个预测层的某几层上同时进行匹配。这样就会导致一个问题:正样本的anchor太少了,导致整体收敛比较慢。后面的FCOS等工作研究表明:增加高质量正样本anchor可以显著加速收敛

YOLO v5和前YOLO系列相比,特点应该是:

  • (1) 增加了正样本:方法是邻域的正样本anchor匹配策略。
  • (2) 通过灵活的配置参数,可以得到不同复杂度的模型
  • (3) 通过一些内置的超参优化策略,提升整体性能
  • (4) 和yolov4一样,都用了mosaic增强,提升小物体检测性能

最后我们对YOLO series总的做个比较,结束这个系列的解读,喜欢的同学点赞关注评论3连:

img

YOLO v5 series的比较

写给小白的Yolo介绍

YOLO是什么?

YOLO是目标检测模型。

目标检测是计算机视觉中比较简单的任务,用来在一张图篇中找到某些特定的物体,目标检测不仅要求我们识别这些物体的种类,同时要求我们标出这些物体的位置

显然,类别是离散数据,位置是连续数据。

img

上面的图片中,分别是计算机视觉的三类任务:分类,目标检测,实例分割。

很显然,整体上这三类任务从易到难,我们要讨论的目标检测位于中间。前面的分类任务是我们做目标检测的基础,至于像素级别的实例分割,太难了别想了。

YOLO在2016年被提出,发表在计算机视觉顶会CVPR(Computer Vision and Pattern Recognition)上,论文的国内镜像在这里:1

YOLO的全称是you only look once,指只需要浏览一次就可以识别出图中的物体的类别和位置。

因为只需要看一次,YOLO被称为Region-free方法,相比于Region-based方法,YOLO不需要提前找到可能存在目标的Region

也就是说,一个典型的Region-base方法的流程是这样的:先通过计算机图形学(或者深度学习)的方法,对图片进行分析,找出若干个可能存在物体的区域,将这些区域裁剪下来,放入一个图片分类器中,由分类器分类。

因为YOLO这样的Region-free方法只需要一次扫描,也被称为单阶段(1-stage)模型。Region-based方法方法也被称为两阶段(2-stage)方法。


YOLO之前的世界

YOLO之前的世界,额,其实是R-CNN什么的,也就是我们前面说的Region-based方法,但是感觉还是太高端了。我们从用脚都能想到的目标检测方法开始讲起。

如果我们现在有一个分类器:

img

这只猫咪好可爱,想撸。

现在我们的追求升级了,我们不仅仅想处理这种一张图片中只有一个物体的图片,我们现在想处理有多个物体的图片。

我们该什么做呢?

首先有几点我们要实现想到:首先物体的位置是不确定的,你没办法保证物体一定在最中间;其次,物体的大小是不确定的,有的物体比较大,也有的物体比较小,注意,这里不是说大象一定更大,猫咪一定更小,毕竟还有近大远小嘛;然后,我们还没办法保证物体的种类,假设我们有一个可以识别100中物体的分类器,那么起码图片中出现了这100种物体我们都要识别出来。

比如说这样:

img

挺难的,是吧?

最naive的方法是滑窗法,就是用滑动窗口去识别一个个物体。

比如这样:

img

上图的红色框框就是所谓的滑窗。如果一个物体正好出现在一个滑窗中,那么我们就可以把它检测出来了,这个滑窗的位置也就是我们认为这个物体所在的位置

等下,如果物体没有正好出现在一个滑窗中呢?

我们管滑窗每次滑动的距离叫做步长,如果我们把步长设置的特别小,如果步长仅仅为一个像素点,那一定可以保证物体可以正好出现在某个窗口中了。

那如果某个物体特别大,或者特别小呢?

例如在上图中,每个窗口和汽车差不多大小,但是如果我们要识别一辆卡车,一个窗口可能就不够大了。

显然,我们可以设计不同大小的窗口,我们可以设计几十中不同大小的窗口,让他们按照最小的步长滑动,把窗口里的所有图片都放入分类器中。

但是这样太太太浪费时间了。

到这里R-CNN同学出现了,他说,你这样用滑窗法可能最后得到了几十万个窗口,而我可以提前扫描一下图片,得到2000个左右的Region(其实就是前面的窗口),这样不就节省了很多时间?

Rich feature hierarchies for accurate object detection and semantic segmentationxxx.itp.ac.cn/pdf/1311.2524

R-CNN同学管这个叫做Region Proposal,并且提出了一个叫做Selective Search的算法。(吐槽一下这个名字起得太大众了)

但是R-CNN被YOLO打脸了,YOLO说,我更快。


YOLO原理

在这之前,我们再重申一下我们的任务。我们的目的是在一张图片中找出物体,并给出它的类别和位置。目标检测是基于监督学习的,每张图片的监督信息是它所包含的N个物体,每个物体的信息有五个,分别是物体的中心位置(x,y)和它的高(h)宽(w),最后是它的类别。

YOLO 的预测是基于整个图片的,并且它会一次性输出所有检测到的目标信息,包括类别和位置。

就好像捕鱼一样,R-CNN是先选好哪里可能出现鱼,而YOLO是直接一个大网下去,把所有的鱼都捞出来。

先假设我们处理的图片是一个正方形

YOLO的第一步是分割图片,它将图片分割为 [公式] 个grid,每个grid的大小都是相等的,像这样:

img

如果我们让每个框只能识别出一个物体,且要求这个物体必须在这个框之内,那YOLO就变成了很蠢的滑窗法了。

YOLO的聪明之处在于,它只要求这个物体的中心落在这个框框之中。

这意味着,我们不用设计非常非常的框,因为我们只需要让物体的中心在这个框中就可以了,而不是必须要让整个物体都在这个框中。

具体怎么实现呢?

我们要让这个 [公式] 个框每个都预测出B个bounding boxs,这个bounding boxs有5个量,分别是物体的中心位置(x,y)和它的高(h)宽(w),以及这次预测的置信度

每个框框不仅只预测B个bounding boxs,它还要负责预测这个框框中的物体是什么类别的,这里的类别用one-hot编码表示。

注意,虽然一个框框有多个bounding boxes,但是只能识别出一个物体,因此每个框框需要预测物体的类别,而bounding box不需要

也就是说,如果我们有 [公式] 个框框,每个框框的bounding boxes个数为B,分类器可以识别出C种不同的物体,那么所有整个ground truth的长度为:

[公式]

先看这些bounding boxs显示出来是什么样的:

img

在上面的例子中,图片被分成了49个框,每个框预测2个bounding boxs,因此上面的图中有98个bounding boxs。

可以看到大致上每个框里确实有两个bounding boxs。

可以看到这些BOX中有的边框比较粗,有的比较细,这是置信度不同的表现,置信度高的比较粗,置信度低的比较细。


在详细的介绍confidence之前,我们先来说一说关于bounding boxs的细节。

bounding boxs可以锁定物体的位置,这要求它输出四个关于位置的值,分别是x,y,h和w。我们在处理输入的图片的时候想让图片的大小任意,这一点对于卷积神经网络来说不算太难,但是,如果输出的位置坐标是一个任意的正实数,模型很可能在大小不同的物体上泛化能力有很大的差异。

这时候当然有一个常见的套路,就是对数据进行归一化,让连续数据的值位于0和1之间。

对于x和y而言,这相对比较容易,毕竟x和y是物体的中心位置,既然物体的中心位置在这个grid之中,那么只要让真实的x除以grid的宽度,让真实的y除以grid的高度就可以了。

但是h和w就不能这么做了,因为一个物体很可能远大于grid的大小,预测物体的高和宽很可能大于bounding boxs的高和宽,这样w除以bounding boxs的宽度,h除以bounding boxs的高度依旧不在0和1之间

解决方法是让w除以整张图片的宽度h除以整张图片的高度

下面的例子是一个448448的图片,有33的grid,展示了计算x,y,w,h的真实值(ground truth)的过程:

img

接下来,我们好好说道说道这个confidence。

confidence的计算公式是:

[公式]

这个IOU的全称是intersection over union,也就是交并比,它反应了两个框框的相似度。

img

[公式] 的意思是预测的bounding box和真实的物体位置的交并比。

[公式] 是一个grid有物体的概率,在有物体的时候ground truth为1,没有物体的时候ground truth为0.

这个[公式] 非常有意思,因为它的groun truth不是确定的,这导致虽然 [公式] 的ground truth是确定的,但是bounding box的confidence的ground truth是不确定的。

一个不确定的ground truth有什么用呢?

想象这样的问题:老师问小明1+1等于几。小明说等于2,老师又问你有多大的把握你的回答是对的,小明说有80%

这里的80%就是confidence。

confidence主要有两个作用,在后面我会一一介绍。

现在,我们根据上面大雁的图片计算一下样本的groun truth:

img

首先,这里有9个grid,每个grid有两个bounding box,每个bounding box有5个预测值,假设分类器可以识别出3中物体,那么ground truth的总长度为 [公式]

我们假定大雁的类别的one-hot为100,另外两个是火鸡和特朗普,分别是010和001.

我们规定每个grid的ground truth的顺序是confidence, x, y, w, h, c1, c2, c3

那么第一个(左上角)grid的ground truth应该是:0, ?, ?, ?, ?, ?, ?, ?

实际上除了最中间的grid以外,其他的grid的ground truth都是这样的。

这里的"?"的意思是,随便是多少都行,我不在乎。在下面我们会看到,我们不会对这些值计算损失函数。

中间的ground truth应该是:

iou, 0.48, 0.28, 0.50, 0.32, 1, 0, 0

iou要根据x, y, w, h的预测值现场计算。


这样看似可以让每个grid找到负责的物体,并把它识别出来了。但是还存在一个不得不考虑的问题,如果物体很大,而框框又很小,一个物体被多个框框识别了怎么办?

这里,我们要用到一个叫做非极大值抑制Non-maximal suppression(NMS)的技术。

这个NMS还是基于交并比实现的。

img

例如在上面狗狗的图里,B1,B2,B3,B4这四个框框可能都说狗狗在我的框里,但是最后的输出应该只有一个框,那怎么把其他框删除呢?

这里就用到了我们之前讲的confidence了,confidence预测有多大的把握这个物体在我的框里,我们在同样是检测狗狗的框里,也就是B1,B2,B3,B4中,选择confidence最大的,把其余的都删掉。

也就是只保留B1.

但是这里还有一个引人深思的问题,为什么confidence的定义是 [公式] ,直接用 [公式] 不行吗,直接用 [公式] 的话就可以把ground truth确定下来,训练的时候就方便多了。

这里有一个非常非常鸡贼的技巧!

理论上只用 [公式] 也可以选出应该负责识别物体的grid,但是可能会不太精确。这里我们训练的目标是预测 [公式] ,我们的想法是让本来不应该预测物体的grid的confidence尽可能的小,既然 [公式] 的效果不太理想,那我就让 [公式] 尽可能小

为什么真正的最中间的grid的confidence往往会比较大呢?

因为我们的bounding boxes是用中点坐标+宽高表示的,每个grid预测的bounding box都要求其中心在这个grid内,那么如果不是最中间的grid,其他的grid的IOU自然而言就会比较低了,因此相应的confidence就降下来了。

现在,我们知道了哪个是应该保留的bounding boxes了,但是还有一个问题,我们是怎么判断出这几个bounding boxes识别的是同一个物体的呢?

这里用到NMS的技巧,我们首先判断这几个grid的类别是不是相同的,假设上面的B1,B2,B3和B4识别的都是狗狗,那么进入下一步,我们保留B1,然后判断B2,B3和B4要不要删除。

我们把B1成为极大bounding box,计算极大bounding box和其他几个bounding box的IOU,如果超过一个阈值,例如0.5,就认为这两个bounding box实际上预测的是同一个物体,就把其中confidence比较小的删除。

最后,我们结合极大bounding box和grid识别的种类,判断图片中有什么物体,它们分别是什么,它们分别在哪。

img

我们刚才说confidence有两个功能,一个是用来极大值抑制,另一个就是在最后输出结果的时候,将某个bounding box的confidencd和这个bounding box所属的grid的类别概率相乘,然后输出。

举个例子,比如某个grid中的某个bounding box预测到了一个物体,将这个bounding box送入神经网络(其实是整张图片一起送进去的,我们这样说是为了方便),然后神经网络对bounding box说,你这里有一个物体的概率是0.8.然后神经网络又对grid说,你这个grid里物体最可能是狗,概率是0.7。

那最后这里是狗的概率就是 [公式]

我们在这里就不细讲yolo的网络结构了,相比之下我认为yolo的损失函数的设计更有创见。

yolo的损失函数是这样的:

[公式]

首先, [公式] 代表的是这个grid里有没有物体,如果这个grid没有物体, [公式] ,反之 [公式]

[公式] 相反的是 [公式] ,如果没有物体, [公式] ,反之 [公式]

说白了就是,当一个grid有物体的时候,损失函数计算第1,2,3,5项,当grid里没有物体的时候计算第4项。

损失函数一共有5项,我们一项一项的分析。

首先的是中心坐标的损失函数,用了我们最熟悉的均方误差MSE,这个很好理解。

然后是高和宽,没有简单的用MSE,而是用平方根的MSE,这是为什么呢?

第一个原因是更容易优化,但是还有更重要的原因:

看下面的表格:

img

首先,我们只考虑var1和var2在0和1之间。当var1和var2都很小的时候,也即是w和h都很小,意味着这个物体很小,那么我们应该尽量放大一些损失函数,让模型在识别小物体的时候准确一点。当var1和var2都很大,意味着这个物体也很大,甚至可能已经布满整张图片了,这时我么可以减小一些损失函数,毕竟很大的物体不需要很高的精度。

一句话,使用平方根的MSE而不是MSE其实就是像让模型对小尺度的物体更敏感。或者说,对大的和小的物体同样敏感。

接下来的三项都使用了MSE,其实用交叉熵可能会更好,但是这些细节就不追究了。

其中第4项是用来判断一个bounding box中究竟有没有物体的。

接下来的问题是 [公式][公式] 都应该取怎样的值,为什么这样设计?

论文中给出的答案是 [公式][公式] ,也就是放大第一项和第二项的损失函数,缩小第四项的损失函数。

这样做的原因是让梯度更稳定,如果grid中不含有物体,它对1,2,3,5项没有影响,如果调节第四项,会让含有物体的grid的confidence发生改变,这可能使其他项的梯度剧烈变化,从而带来模型上的不稳定。

因此,我们放大第一项和第二项,缩小第四项。

YOLO的设计虽然精巧,但是还有许多不足的地方,比如一个grid只能识别出一种物体。我们会在YOLO v2和YOLO v3中看到更巧妙的设计。

此外,YOLO的bounding box的预测准确度是不如R-CNN这样的Region-based方法的,看YOLO v2里作者们怎么报复回来。