赞
踩
论文题目:YOLOv3: An Incremental Improvement(翻译过来就是一个渐进式的改进)
论文下载地址: https://arxiv.org/abs/1804.02767
在学习yolov3的时候,我们先假设我们已经对神经网络有所了解,至少应该学过alexnet和vgg这些网络,这样的话,我们的讲解才能清晰透彻。
在320 × 320的情况下,YOLOv3在28.2 mAP下运行时间为22 ms,精度与SSD相当,但速度快3倍,这是yolov3的作者在摘要中写的,意思是在输入为320*320图片的情况下,yoloV3的运行速度是SSD的3倍,精度也并不会比SSD差。
目标检测作为一个比图像识别更复杂的算法,我们要做的不仅仅是将图像识别出来,还要将目标的位置定位出来,也就是我们不仅要识别目标的种类,还有检测目标的位置,这也就是为啥目标检测相对复杂一点的原因,在yolo系列代码中,相较于yolo1和yolo2,yolo3的网络结构是有巨大的差别的,这也是为啥在yolov3出来之后,官方就下架了yolo1和yolo2的代码,而之后的yolo4和yolo5等等都是在yolo3的基础上加入了一些比较新的trick融合而成,因此我们要学习yolo系列的目标检测算法,yolo3一定是我们需要第一个学习的。
这是在COCO数据集上,yolo v3相较于当时的一些其他的目标检测算法的指标对比图,从中我们可以清晰的看到不论是在mAP还是time上,yolov3都明显由于当时的一些其他目标检测算法。
首先,我门先看一下yolov3的网络结构,在这里如果觉得难以理解的话可以把他当成一个类似于alexnet和vgg16的简单神经网络结构,事实上也确实是如此,因为目标检测算法和普通的卷积神经分类网络一样都是输入图片进行训练,然后预测,就是不同在于yolo在将图像打入网络进行训练之前需要进行一系列复杂的处理,毕竟我们不光要对目标进行识别,还要定位检测。yolov3的创新之处在于内部使用了借鉴Resnet的残差网络结构进行跳跃链接(具体的想了解的可以读源码,不知道什么是残差借鉴的可以先学习一下Resnet)且,且最后的输出是分成了三个不同的特征层进行输出,最后进行特征下采样,
最后三层三个输出,利用三个特征层进行边框的预测。
想学习Resnet的可以看我的Resnet的讲解:手撕Resnet卷积神经网络-pytorch-详细注释版(可以直接替换自己数据集)-直接放置自己的数据集就能直接跑。跑的代码有问题的可以在评论区指出,看到了会回复。训练代码和预测代码均有。_小馨馨的小翟的博客-CSDN博客
好了 下面我们开始讲这个模型结构,yolov3的主体模型结构叫做Darknet-53。这里之所以叫53的意思是这个网络结构有53个卷积层。我们可以数一下,每个Convolutional代表一个卷积层,旁边的数代表这个卷积层有几个类似的。且最下面的Connected的全连接层也代表一个卷积层,因此(2+1*2+1+2*2+1+2*8+1+2*8+1+4*2+1)= 53
另外还有一点需要强调的是网络结构图中每个卷积层Convolutional的完整构成是:
(卷积层 + BN层 + LeakyRelu层) = Convolutional
这里我们简单介绍一下LeakyReLU激活函数:
与Relu激活将负数部分前部置为0不一样的是,LeakyReLU激活函数则是保留负数部分对模型带来的一点影响,给负数部分一个很小的非零的倾斜率,让其对模型保留相应的影响效果,不会完全失效。
从网络结构图可以看出这些卷积核都是有3*3和1*1的卷积核构成。yolov3的很大的一个改进之处是借鉴了Resnet的残差结构,残差结构的好处是可以增加相当部分的网络深度而不会出现梯度爆炸等问题,因此可以加深网络进行提高网络的性能。
在论文中作者关于这个网络结构的描述很模糊,我还是看了其他大佬的讲解和源代码才弄清楚Darknet-53 的结构。
下面是Darknet-53 的pytorch源代码
- class BasicBlock(nn.Module):
- def __init__(self, inplanes, planes):
- super(BasicBlock, self).__init__()
- self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
- self.bn1 = nn.BatchNorm2d(planes[0])
- self.relu1 = nn.LeakyReLU(0.1)
-
- self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3, stride=1, padding=1, bias=False)
- self.bn2 = nn.BatchNorm2d(planes[1])
- self.relu2 = nn.LeakyReLU(0.1)
-
- def forward(self, x):
- residual = x
-
- out = self.conv1(x)
- out = self.bn1(out)
- out = self.relu1(out)
-
- out = self.conv2(out)
- out = self.bn2(out)
- out = self.relu2(out)
-
- out += residual
- return out
-
- class DarkNet(nn.Module):
- def __init__(self, layers):
- super(DarkNet, self).__init__()
- self.inplanes = 32
- # 416,416,3 -> 416,416,32
- self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)
- self.bn1 = nn.BatchNorm2d(self.inplanes)
- self.relu1 = nn.LeakyReLU(0.1)
-
- # 416,416,32 -> 208,208,64
- self.layer1 = self._make_layer([32, 64], layers[0])
- # 208,208,64 -> 104,104,128
- self.layer2 = self._make_layer([64, 128], layers[1])
- # 104,104,128 -> 52,52,256
- self.layer3 = self._make_layer([128, 256], layers[2])
- # 52,52,256 -> 26,26,512
- self.layer4 = self._make_layer([256, 512], layers[3])
- # 26,26,512 -> 13,13,1024
- self.layer5 = self._make_layer([512, 1024], layers[4])
-
- self.layers_out_filters = [64, 128, 256, 512, 1024]
-
- # 进行权值初始化
- for m in self.modules():
- if isinstance(m, nn.Conv2d):
- n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
- m.weight.data.normal_(0, math.sqrt(2. / n))
- elif isinstance(m, nn.BatchNorm2d):
- m.weight.data.fill_(1)
- m.bias.data.zero_()
-
- #---------------------------------------------------------------------#
- # 在每一个layer里面,首先利用一个步长为2的3x3卷积进行下采样
- # 然后进行残差结构的堆叠
- #---------------------------------------------------------------------#
- def _make_layer(self, planes, blocks):
- layers = []
- # 下采样,步长为2,卷积核大小为3
- layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False)))
- layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
- layers.append(("ds_relu", nn.LeakyReLU(0.1)))
- # 加入残差结构
- self.inplanes = planes[1]
- for i in range(0, blocks):
- layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
- return nn.Sequential(OrderedDict(layers))
-
- def forward(self, x):
- x = self.conv1(x)
- x = self.bn1(x)
- x = self.relu1(x)
-
- x = self.layer1(x)
- x = self.layer2(x)
- out3 = self.layer3(x)
- out4 = self.layer4(out3)
- out5 = self.layer5(out4)
-
- return out3, out4, out5 #这就是最后三个特征层的输出,输出提取的三个不同的特征
-
- def darknet53():
- model = DarkNet([1, 2, 8, 8, 4])
- return model
如果光看论文的结构图感觉看不懂网络结构的可以读一下源代码就会清晰明了。(源码中是以416*416作为输入进行写的代码)
这就涉及到yolo的另一个定位的功能了,因为跟传统的目标检测不同的是,我们不仅需要识别目标的种类,还要对目标的位置进行定位。
σ(x)函数是sigmoid函数其目的是将预测的结果缩放到0到1之间。这里虚线框为anchor先验框的模板,pw,ph是模板的宽和高,tx,ty,tw,th分别为我们网络预测的偏移之后的目标中心坐标和需要对应到长款bw,bh的关键参数tw,th,这里可以理解为长的关键参数tw(即为影响长的缩放参数),宽的关键参数tw(即为影响宽的缩放参数)。
最终边界框的中心坐标为(bx,by),预测的目标框的长宽为bw,bh。
具体的计算方式如下:
简单总结一下分三步:
第一步:我们会在网络最开始的设置的时候得到pw,ph(先验框的长宽是最开始设置好的),也就是所谓anchor的长宽,Cx,Cy为Grid Cell(单元格)左上角的坐标
第二步:我们经过模型的预测获取tx,ty,tw,th四个参数
第三步:将参数进行如下图的运算,最终边界框的中心坐标为(bx,by),预测的目标框的长宽为bw,bh。
sigmoid函数其目的是将预测的结果缩放到0到1之间(这样能够将每个Grid Cell(单元格)中预测的边界框的中心坐标保持在在当前Grid Cell(单元格)当中)。因为如果不这样做,可能tx或者ty太大会导致偏移过大超出当前单元格,这就不是我们想看到的结果了。
注明:参考b站霹雳大神的ppt
yolov3的损失像他的名字一样,分为三种
all——loss = x1*目标定位偏移损失 + x2*目标分类损失 + x3*置信度confidence损失
x1 , x2 , x3为平衡系数
yolov3采用的是二值交叉熵损失(按照我的理解是,这不就是跟经典CNN模型一样嘛,这样yolov3不仅检测效果好,识别效果也好)
这里这个IOU是交并比的意思,即为真实目标与预测目标交并的部分比上两者全部的部分,
真实目标矩形框是事先标注好的GTBox,默认矩形框为Anchor,预测的目标矩形框就是网络最终预测的结果(最终的结果 = 网络预测的回归参数在默认矩形框上调整的结果)
gx , gy , gw , gh 分别代表GT Boxes中心点的坐标x, y以及宽度和高度(对应到grid cell单元格中)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。