一、YoloV3实现思路
整个YoloV3可以分为三个部分,分别是Darknet53,FPN以及Yolo Head。
Darknet53可以被称作YoloV3的主干特征提取网络,输入的图片首先会在Darknet53里面进行特征提取,提取到的特征可以被称作特征层,是输入图片的特征集合。在主干部分,我们获取了三个特征层进行下一步网络的构建,这三个特征层我称它为有效特征层。
FPN可以被称作YoloV3的加强特征提取网络,在主干部分获得的三个有效特征层会在这一部分进行特征融合,特征融合的目的是结合不同尺度的特征信息。在FPN部分,已经获得的有效特征层被用于继续提取特征。
Yolo Head是YoloV3的分类器与回归器,通过Darknet53和FPN,我们已经可以获得三个加强过的有效特征层,他们的shape分别为(52,52,128),(26,26,256),(13,13,512)。每一个特征层都有宽、高和通道数,此时我们可以将特征图看作一个又一个特征点的集合,每一个特征点都有通道数个特征。Yolo Head实际上所做的工作就是对特征点进行判断,判断特征点是否有物体与其对应。
因此,整个YoloV3网络所作的工作就是 特征提取-特征加强-预测特征点对应的物体情况。
1、主干网络Darknet53介绍
YoloV3所使用的主干特征提取网络为Darknet53,它具有两个重要特点:
1.)使用了残差网络Residual,Darknet53中的残差卷积可以分为两个部分,主干部分是一次1X1的卷积(利用一个1x1卷积下降通道数)和一次3X3的卷积(3x3卷积提取特征并且上升通道数);残差边部分不做任何处理,直接将主干的输入与输出结合。整个YoloV3的主干部分都由残差卷积构成,上述所示的YoloV3整体结构里,Resblock_body后面的x几就代表在这个特征层部分,残差结构重复了几次。
残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。
2.)Darknet53的每一个DarknetConv2D后面都紧跟了BatchNormalization标准化与LeakyReLU部分。普通的ReLU是将所有的负值都设为零,Leaky ReLU则是给所有负值赋予一个非零斜率。以数学的方式我们可以表示为**:
2、构建FPN特征金字塔进行加强特征提取
在特征利用部分,YoloV3提取多特征层进行目标检测,一共提取三个特征层。
三个特征层位于主干部分Darknet53的不同位置,分别位于中间层,中下层,底层,三个特征层的shape分别为(52,52,256)、(26,26,512)、(13,13,1024)。
在获得三个有效特征层后,我们利用这三个有效特征层进行FPN层的构建,构建方式为:
13x13x1024的特征层进行5次卷积处理,处理完后利用YoloHead获得预测结果,一部分用于进行上采样UmSampling2d后与26x26x512特征层进行结合,结合特征层的shape为(26,26,768)。
结合特征层再次进行5次卷积处理,处理完后利用YoloHead获得预测结果,一部分用于进行上采样UmSampling2d后与52x52x256特征层进行结合,结合特征层的shape为(52,52,384)。
结合特征层再次进行5次卷积处理,处理完后利用YoloHead获得预测结果。
特征金字塔可以将不同shape的特征层进行特征融合,有利于提取出更好的特征。
3、利用Yolo Head获得预测结果
利用FPN特征金字塔,我们可以获得三个加强特征,这三个加强特征的shape分别为(13,13,512)、(26,26,256)、(52,52,128),然后我们利用这三个shape的特征层传入Yolo Head获得预测结果。
Yolo Head本质上是一次3x3卷积加上一次1x1卷积,3x3卷积的作用是特征整合,1x1卷积的作用是调整通道数。
对三个特征层分别进行处理,假设我们预测是的VOC数据集,我们的输出层的shape分别为(13,13,75),(26,26,75),(52,52,75),最后一个维度为75是因为该图是基于voc数据集的,它的类为20种,YoloV3针对每一个特征层的每一个特征点存在3个先验框,所以预测结果的通道数为3x25;
如果使用的是coco训练集,类则为80种,最后的维度应该为255 = 3x85,三个特征层的shape为(13,13,255),(26,26,255),(52,52,255)
其实际情况就是,输入N张416x416的图片,在经过多层的运算后,会输出三个shape分别为(N,13,13,255),(N,26,26,255),(N,52,52,255)的数据,对应每个图分为13x13、26x26、52x52的网格上3个先验框的位置。
darknet.py
1 import math 2 from collections import OrderedDict 3 4 import torch.nn as nn 5 6 #---------------------------------------------------------------------# 7 # 残差结构 8 # 利用一个1x1卷积下降通道数,然后利用一个3x3卷积提取特征并且上升通道数 9 # 最后接上一个残差边 10 #---------------------------------------------------------------------# 11 class BasicBlock(nn.Module): 12 def __init__(self, inplanes, planes): 13 super(BasicBlock, self).__init__() 14 self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False) 15 self.bn1 = nn.BatchNorm2d(planes[0]) 16 self.relu1 = nn.LeakyReLU(0.1) 17 18 self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3, stride=1, padding=1, bias=False) 19 self.bn2 = nn.BatchNorm2d(planes[1]) 20 self.relu2 = nn.LeakyReLU(0.1) 21 22 def forward(self, x): 23 residual = x 24 25 out = self.conv1(x) 26 out = self.bn1(out) 27 out = self.relu1(out) 28 29 out = self.conv2(out) 30 out = self.bn2(out) 31 out = self.relu2(out) 32 33 out += residual 34 return out 35 36 class DarkNet(nn.Module): 37 def __init__(self, layers): 38 super(DarkNet, self).__init__() 39 self.inplanes = 32 40 # 416,416,3 -> 416,416,32 41 self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False) 42 self.bn1 = nn.BatchNorm2d(self.inplanes) 43 self.relu1 = nn.LeakyReLU(0.1) 44 45 # 416,416,32 -> 208,208,64 46 self.layer1 = self._make_layer([32, 64], layers[0]) # layers[1, 2, 8, 8, 4] 47 # 208,208,64 -> 104,104,128 48 self.layer2 = self._make_layer([64, 128], layers[1]) 49 # 104,104,128 -> 52,52,256 50 self.layer3 = self._make_layer([128, 256], layers[2]) 51 # 52,52,256 -> 26,26,512 52 self.layer4 = self._make_layer([256, 512], layers[3]) 53 # 26,26,512 -> 13,13,1024 54 self.layer5 = self._make_layer([512, 1024], layers[4]) 55 56 self.layers_out_filters = [64, 128, 256, 512, 1024] 57 58 # 进行权值初始化 59 for m in self.modules(): 60 if isinstance(m, nn.Conv2d): 61 n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels 62 m.weight.data.normal_(0, math.sqrt(2. / n)) 63 elif isinstance(m, nn.BatchNorm2d): 64 m.weight.data.fill_(1) 65 m.bias.data.zero_() 66 67 #---------------------------------------------------------------------# 68 # 在每一个layer里面,首先利用一个步长为2的3x3卷积进行下采样 69 # 然后进行残差结构的堆叠 70 #---------------------------------------------------------------------# 71 def _make_layer(self, planes, blocks): 72 layers = [] 73 # 下采样,步长为2,卷积核大小为3 74 layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False))) 75 layers.append(("ds_bn", nn.BatchNorm2d(planes[1]))) 76 layers.append(("ds_relu", nn.LeakyReLU(0.1))) 77 # 加入残差结构 78 self.inplanes = planes[1] 79 for i in range(0, blocks): 80 layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes))) 81 return nn.Sequential(OrderedDict(layers)) 82 83 def forward(self, x): 84 x = self.conv1(x) 85 x = self.bn1(x) 86 x = self.relu1(x) 87 88 x = self.layer1(x) 89 x = self.layer2(x) 90 out3 = self.layer3(x) 91 out4 = self.layer4(out3) 92 out5 = self.layer5(out4) 93 94 return out3, out4, out5 95 96 def darknet53(): 97 model = DarkNet([1, 2, 8, 8, 4]) 98 return modelView Code
yolo.py
1 from collections import OrderedDict 2 3 import torch 4 import torch.nn as nn 5 6 from nets.darknet import darknet53 7 8 def conv2d(filter_in, filter_out, kernel_size): 9 pad = (kernel_size - 1) // 2 if kernel_size else 0 10 return nn.Sequential(OrderedDict([ 11 ("conv", nn.Conv2d(filter_in, filter_out, kernel_size=kernel_size, stride=1, padding=pad, bias=False)), 12 ("bn", nn.BatchNorm2d(filter_out)), 13 ("relu", nn.LeakyReLU(0.1)), 14 ])) 15 16 #------------------------------------------------------------------------# 17 # make_last_layers里面一共有七个卷积,前五个用于提取特征。 18 # 后两个用于获得yolo网络的预测结果 19 #------------------------------------------------------------------------# 20 def make_last_layers(filters_list, in_filters, out_filter): 21 m = nn.Sequential( 22 conv2d(in_filters, filters_list[0], 1), 23 conv2d(filters_list[0], filters_list[1], 3), 24 conv2d(filters_list[1], filters_list[0], 1), 25 conv2d(filters_list[0], filters_list[1], 3), 26 conv2d(filters_list[1], filters_list[0], 1), 27 conv2d(filters_list[0], filters_list[1], 3), 28 nn.Conv2d(filters_list[1], out_filter, kernel_size=1, stride=1, padding=0, bias=True) 29 ) 30 return m 31 32 class YoloBody(nn.Module): 33 def __init__(self, anchors_mask, num_classes, pretrained = False): 34 super(YoloBody, self).__init__() 35 #---------------------------------------------------# 36 # 生成darknet53的主干模型 37 # 获得三个有效特征层,他们的shape分别是: 38 # 52,52,256 39 # 26,26,512 40 # 13,13,1024 41 #---------------------------------------------------# 42 self.backbone = darknet53() 43 if pretrained: 44 self.backbone.load_state_dict(torch.load("model_data/darknet53_backbone_weights.pth")) 45 46 #---------------------------------------------------# 47 # out_filters : [64, 128, 256, 512, 1024] 48 #---------------------------------------------------# 49 out_filters = self.backbone.layers_out_filters 50 51 #------------------------------------------------------------------------# 52 # 计算yolo_head的输出通道数,对于voc数据集而言 53 # final_out_filter0 = final_out_filter1 = final_out_filter2 = 75 54 #------------------------------------------------------------------------# 55 self.last_layer0 = make_last_layers([512, 1024], out_filters[-1], len(anchors_mask[0]) * (num_classes + 5)) 56 57 self.last_layer1_conv = conv2d(512, 256, 1) 58 self.last_layer1_upsample = nn.Upsample(scale_factor=2, mode='nearest') 59 self.last_layer1 = make_last_layers([256, 512], out_filters[-2] + 256, len(anchors_mask[1]) * (num_classes + 5)) 60 61 self.last_layer2_conv = conv2d(256, 128, 1) 62 self.last_layer2_upsample = nn.Upsample(scale_factor=2, mode='nearest') 63 self.last_layer2 = make_last_layers([128, 256], out_filters[-3] + 128, len(anchors_mask[2]) * (num_classes + 5)) 64 65 def forward(self, x): 66 #---------------------------------------------------# 67 # 获得三个有效特征层,他们的shape分别是: 68 # 52,52,256;26,26,512;13,13,1024 69 #---------------------------------------------------# 70 x2, x1, x0 = self.backbone(x) 71 72 #---------------------------------------------------# 73 # 第一个特征层 74 # out0 = (batch_size,255,13,13) 75 #---------------------------------------------------# 76 # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 77 out0_branch = self.last_layer0[:5](x0) 78 out0 = self.last_layer0[5:](out0_branch) 79 80 # 13,13,512 -> 13,13,256 -> 26,26,256 81 x1_in = self.last_layer1_conv(out0_branch) 82 x1_in = self.last_layer1_upsample(x1_in) 83 84 # 26,26,256 + 26,26,512 -> 26,26,768 85 x1_in = torch.cat([x1_in, x1], 1) 86 #---------------------------------------------------# 87 # 第二个特征层 88 # out1 = (batch_size,255,26,26) 89 #---------------------------------------------------# 90 # 26,26,768 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 91 out1_branch = self.last_layer1[:5](x1_in) 92 out1 = self.last_layer1[5:](out1_branch) 93 94 # 26,26,256 -> 26,26,128 -> 52,52,128 95 x2_in = self.last_layer2_conv(out1_branch) 96 x2_in = self.last_layer2_upsample(x2_in) 97 98 # 52,52,128 + 52,52,256 -> 52,52,384 99 x2_in = torch.cat([x2_in, x2], 1) 100 #---------------------------------------------------# 101 # 第一个特征层 102 # out3 = (batch_size,255,52,52) 103 #---------------------------------------------------# 104 # 52,52,384 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128 105 out2 = self.last_layer2(x2_in) 106 return out0, out1, out2View Code
二、预测结果的解码
1、什么是先验框
由网络我们可以获得三个特征层的预测结果,shape分别为: (N,13,13,255)、 (N,26,26,255)、(N,52,52,255),N代表的是batch_size,就是输入图片的数量,我们可以忽略,但是后面的(52,52,255)、(26,26,255)、(13,13,255),就不可以忽略了。每一个预测结果都有宽、高和通道数,宽、高里面是一个又一个特征点,那此时我们便可以想办法利用这些特征点,和原图进行结合。
我们再看看预测结果的特点,13X13,26X26,52X52,它是不是非常像三个等分的网格?如果我们将原图划分成对应13X13,26X26,52X52的部分,是不是整个特征层就以某种形式映射在原图上了。事实上yolo系列就是这么做的,每一个有效特征层将整个图片分成与其长宽对应的网格,仔细看看这幅图,原图被划分成13x13的网格;然后从每个网格中心建立多个先验框,典型值是一个特征点三个先验框,这些框是网络预先设定好的框,网络的预测结果会判断这些框内是否包含物体,以及这个物体的种类。
2. 获得先验框后做什么
由网络我们可以获得三个特征层的预测结果,shape分别为: (N,13,13,255)、 (N,26,26,255)、 (N,52,52,255);
由于每一个网格点都具有三个先验框,所以上述的预测结果可以reshape为: (N,13,13,3,85)、(N,26,26,3,85) 、(N,52,52,3,85)
其中的85可以拆分为4+1+80,其中的4代表先验框的调整参数,1代表先验框内是否包含物体,80代表的是这个先验框的种类,由于coco分了80类,所以这里是80。如果YoloV3只检测两类物体,那么这个85就变为了4+1+2 = 7。即85包含了4+1+80,分别代表x_offset、y_offset、h和w、置信度、分类结果。但是这个预测结果并不对应着最终的预测框在图片上的位置,还需要解码才可以完成。
YoloV3的解码过程分为两步:
先将每个网格点加上它对应的x_offset和y_offset,加完后的结果就是预测框的中心。
然后再利用 先验框和h、w结合 计算出预测框的宽高。这样就能得到整个预测框的位置了。
下图展示了YoloV3解码的过程:
3、得分筛选与非极大抑制
得到最终的预测结果后还要进行得分排序与非极大抑制筛选。
得分筛选就是筛选出得分满足confidence置信度的预测框。
非极大抑制就是筛选出一定区域内属于同一种类得分最大的框。
得分筛选与非极大抑制的过程可以概括如下:
1、找出该图片中得分大于门限函数的框。在进行重合框筛选前就进行得分的筛选可以大幅度减少框的数量。
2、对种类进行循环,非极大抑制的作用是筛选出一定区域内属于同一种类得分最大的框,对种类进行循环可以帮助我们对每一个类分别进行非极大抑制。
3、根据得分对该种类进行从大到小排序。
4、每次取出得分最大的框,计算其与其它所有预测框的重合程度,重合程度过大的则剔除。
得分筛选与非极大抑制后的结果就可以用于绘制预测框了。
下图是经过非极大抑制的。
下图是未经过非极大抑制的。
四、训练部分
1、计算loss所需参数
在计算loss的时候,实际上是y_pre和y_true之间的对比:
y_pre就是一幅图像经过网络之后的输出,内部含有三个特征层的内容;其需要解码才能够在图上作画
y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
实际上y_pre和y_true内容的shape都是
(batch_size,13,13,3,85)
(batch_size,26,26,3,85)
(batch_size,52,52,3,85)
2、y_pre是什么
对于YoloV3的模型来说,网络最后输出的内容就是三个特征层每个网格点对应的预测框及其种类,即三个特征层分别对应着图片被分为不同size的网格后,每个网格点上三个先验框对应的位置、置信度及其种类。
对于输出的y1、y2、y3而言,[…, : 2]指的是相对于每个网格点的偏移量,[…, 2: 4]指的是宽和高,[…, 4: 5]指的是该框的置信度,[…, 5: ]指的是每个种类的预测概率。
现在的y_pre还是没有解码的,解码了之后才是真实图像上的情况。
3、y_true是什么。
y_true就是一个真实图像中,它的每个真实框对应的(13,13)、(26,26)、(52,52)网格上的偏移位置、长宽与种类。其仍需要编码才能与y_pred的结构一致
在YoloV3中,其使用了一个专门的函数用于处理读取进来的图片的框的真实情况。
def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
其输入为:
true_boxes:shape为(m, T, 5)代表m张图T个框的x_min、y_min、x_max、y_max、class_id。
input_shape:输入的形状,此处为416、416
anchors:代表9个先验框的大小
num_classes:种类的数量。
其实对真实框的处理是将真实框转化成图片中相对网格的xyhw,步骤如下:
1、取框的真实值,获取其框的中心及其宽高,除去input_shape变成比例的模式。
2、建立全为0的y_true,y_true是一个列表,包含三个特征层,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。
3、对每一张图片处理,将每一张图片中的真实框的wh和先验框的wh对比,计算IOU值,选取其中IOU最高的一个,得到其所属特征层及其网格点的位置,在对应的y_true中将内容进行保存。
4、loss的计算过程
在得到了y_pre和y_true后怎么对比呢?不是简单的减一下就可以的呢。
loss值需要对三个特征层进行处理,这里以最小的特征层为例。
1、利用y_true取出该特征层中真实存在目标的点的位置(m,13,13,3,1)及其对应的种类(m,13,13,3,80)。
2、将yolo_outputs的预测值输出进行处理,得到reshape后的预测值y_pre,shape分别为(m,13,13,3,85),(m,26,26,3,85),(m,52,52,3,85)。还有解码后的xy,wh。
3、获取真实框编码后的值,后面用于计算loss,编码后的值其含义与y_pre相同,可用于计算loss。
4、对于每一幅图,计算其中所有真实框与预测框的IOU,取出每个网络点中IOU最大的先验框,如果这个最大的IOU都小于ignore_thresh,则保留,一般来说ignore_thresh取0.5,该步的目的是为了平衡负样本。
5、计算xy和wh上的loss,其计算的是实际上存在目标的,利用第三步真实框编码后的的结果和未处理的预测结果进行对比得到loss。
6、计算置信度的loss,其有两部分构成,第一部分是实际上存在目标的,预测结果中置信度的值与1对比;第二部分是实际上不存在目标的,在第四步中得到其最大IOU的值与0对比。
7、计算预测种类的loss,其计算的是实际上存在目标的,预测类与真实类的差距。
其实际上计算的总的loss是三个loss的和,这三个loss分别是:
实际存在的框,编码后的长宽与xy轴偏移量与预测值的差距。
实际存在的框,预测结果中置信度的值与1对比;实际不存在的框,在上述步骤中,在第四步中得到其最大IOU的值与0对比。
实际存在的框,种类预测结果与实际结果的对比。
代码参考链接:https://github.com/bubbliiiing/yolo3-pytorch