YOLO 核心思想
- 整张图作为网络的输入,直接在输出层用回归的方式得到bounding box的位置和bounding box所属的类别。
- 之前的一些RCNN算法都是采用的是proposal+classification的思想
YOLO的实现
- 先将图片分成S×S的网格(grid cell),如果某个object的中心落在这个网格中,则这个网格就负责预测这个object
- 每个网格要预测B个bounding box,每个bounding box除了要回归自身位置(x,y,w,h)还要回归一个confidence值。
这个confidence代表了所预测的box中含有object的置信度和这个box预测的有多准的双重信息,其值是这样计算的:其中如果有object落在一个grid cell,则第一项取1,否则取0.第二项是预测的bounding box和实际的ground truth之间的IOU值。 - 每个bounding box要预测(x,y,w,h)和confidence共5个值,每个网格还要要预测一个类别信息,记为C类,则S×S个网格,每个网格要预测B个Bounding box还要预测C个类别。输入一张图,输出的是一个S×S×(5×B+C)的一个tensor.
注意:class类别信息是针对每个网格的,confidence信息是针对每个bounding box的
得到每个bbox的class-specific confidence score以后,设置阈值,滤掉得分低的boxes,对保留的boxes进行NMS处理,就得到最终的预测结果。
举例
网络流程图:
在PASCAL VOC中,输入图像为448×448,S=7,B=2,共20个类别。 输出的tensor为7×7×30.
整个网络的图:
每个网格有30维,这30维中,8维是回归box的坐标,2维是box的置信区间,还有20维是类别。
在test的时候,每个网格预测的class信息和bounding box预测的confidence信息相乘,就得到每个bounding box的class-specific confidence score:
等式左边第一项是每个网格预测的类别信息,第二第三项是每个bounding box预测的confidence.两者相乘,得到的是某个box属于某一类的概率。
在得到每个box的class-specific confidence score 以后,设置阈值,滤掉分低的boxes,对保留的boxes进行NMS处理,得到最终的检测结果。
NMS
这个算法不单单是针对YOLO算法的,而是所有的检测算法都会用到。NMS算法主要解决的是一个目标被多次检测的问题.
- 1.首先从所有的检测框中找到该类置信度最大的那个框,
- 2.然后挨个计算其与剩余框的IOU,如果其值大于一定阈值(重合度过高),那就剔除该框。(将其置信度置为0)
- 3.然后对剩余的检测框重复上述过程(跳过那些已经置为0的框),直到处理完所有的检测框。
YOLO实现细节
- 1.每个网格有30维,这30维中,8维是回归box的坐标,2维是box的confidence,还有20维是类别。其中坐标的x,y用对应网格的offset归一化到0-1之间,w,h用图像的width和height归一化到0-1之间。
- 2.实现时如何设计损失函数平衡好三方面?作者简单粗暴的全部采用了sum-squared error loss来做这件事。这样会有问题:First: 8维的localization error和20维的classification error等同重要显然不合理. Second:如果一个网格中没有object,那么就会将这些网格中的box的confidence push到0,相比于较少的有object的网格,这种做法是overpowering的,这会导致网络不稳定甚至发散。因此做3、4操作
- 3.更重视8维的坐标预测,给这些损失前面赋予更大的loss weight,记为$\lambda_{coord}$在pascal VOC训练中取5.
- 4.对没有object的box的confidence loss,赋予小的loss weight,记为$\lambda_{noobj}$在pascal VOC训练中取0.5.
5.有object的box的confidence loss和类别的loss的loss weight正常取1.
6.对于不同大小的box预测中,相比于大box预测偏一点,小box预测偏一点肯定更不能被忍受。而sum-square error loss中同样的偏移loss是一样的。
为了缓和这个问题,作者采用了一个比较取巧的方法,将box的width和height取平方根代替原本的height和width.- 7.一个网格预测多个box,希望的是每个box predictor专门负责预测某个object.具体做法就是看当前预测的box与ground truth box中哪个IOU大,就负责哪个。这种做法称作box predictor的specialization.
8.最后整个损失函数如下所示:
第一个和第二个双求和符号用来做坐标预测,$1_{ij}^{obj}$表示判断第i个网格中的第j个box是否负责这个object
第二个双求和符号里表示的是含object的box的confidence预测
第三个双求和符号里表示的是不含object的box的confidence预测
第四个双求和符号里表示的是雷贝预测,$1_{i}^{obj}$表示判断是否有object中心落在网格i中
YOLO的缺点
- YOLO对互相靠的很近的物体,还有很小的群体检测效果不好,这是因为一个网格中只预测两个框,并且只属于一个类
- 对测试图像中,同一类物体出现的新的不常见的长宽比和其他情况。泛化能力偏弱
- 由于损失函数的问题,定位误差是影响检测效果的主要原因。尤其是大小物体的处理上,还有待加强。
Code
YOLOLoss
1 | import torch |
YOLO的优势就在于它可以直接将图片丢入网络中,直接回归得到它的坐标未知以及类别.所以代码的难点在于上述的YOLOLoss的计算.至于网络部分没有什么难度.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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83import torch.nn as nn
class Flatten(nn.Module):
def __init__(self):
super(Flatten, self).__init__()
def forward(self, x):
return x.view(x.size(0), -1)
class YOLO_V1(nn.Module):
def __init__(self):
super(YOLO_V1, self).__init__()
C = 20 # number of classes
print("\n------Initiating YOLO v1------\n")
self.conv_layer1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=7//2),
nn.BatchNorm2d(64),
nn.LeakyReLU(0.1),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_layer2 = nn.Sequential(
nn.Conv2d(in_channels=64, out_channels=192, kernel_size=3, stride=1, padding=3//2),
nn.BatchNorm2d(192),
nn.LeakyReLU(0.1),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_layer3 = nn.Sequential(
nn.Conv2d(in_channels=192, out_channels=128, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=3//2),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.1),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_layer4 = nn.Sequential(
nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.BatchNorm2d(1024),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.conv_layer5 = nn.Sequential(
nn.Conv2d(in_channels=1024, out_channels=512, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=1024, out_channels=512, kernel_size=1, stride=1, padding=1//2),
nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=1024, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=1024, out_channels=1024, kernel_size=3, stride=2, padding=3//2),
nn.BatchNorm2d(1024),
nn.LeakyReLU(0.1),
)
self.conv_layer6 = nn.Sequential(
nn.Conv2d(in_channels=1024, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.Conv2d(in_channels=1024, out_channels=1024, kernel_size=3, stride=1, padding=3//2),
nn.BatchNorm2d(1024),
nn.LeakyReLU(0.1)
)
self.flatten = Flatten()
self.conn_layer1 = nn.Sequential(
nn.Linear(in_features=7*7*1024, out_features=4096),
nn.Dropout(),
nn.LeakyReLU(0.1)
)
self.conn_layer2 = nn.Sequential(nn.Linear(in_features=4096, out_features=7 * 7 * (2 * 5 + C)))
def forward(self, input):
conv_layer1 = self.conv_layer1(input)
conv_layer2 = self.conv_layer2(conv_layer1)
conv_layer3 = self.conv_layer3(conv_layer2)
conv_layer4 = self.conv_layer4(conv_layer3)
conv_layer5 = self.conv_layer5(conv_layer4)
conv_layer6 = self.conv_layer6(conv_layer5)
flatten = self.flatten(conv_layer6)
conn_layer1 = self.conn_layer1(flatten)
output = self.conn_layer2(conn_layer1)
return output