yolov3 推理所需要的时间_目标检测-番外五:YOLOv3-Plus
最近好不容易有了空闲时间,便对之前的项目做了些调整和维护,主要包括:
1.修改了数据预处理方式
以前都是直接resize成方块,不考虑长宽比畸变的问题。现在按照官方v3的操作,会用padding补零的方式将图片填充成方块,再resize(或者resize的时候保住长宽比,再填充0)。
2.加入SPP结构,升级为YOLOv3-SPP
拟定加入更多的trick。
3.multi scale trick不能使用多个workers的bug已解决
办法很简单:先容易将一批数据预处理成640x640的,再去插值到416、512、608等多个尺度。这个操作我是借鉴了yolov5。
目前,在voc上完成了实验:
比我之前的那种实现略好一些,精度没损失,反而训练速度可以更快(可以用更多的workers来加快预处理过程。)。
COCO实验已出,陆续更新:
目前,在COCO val上获得的AP结果如下:
输入:416
推理速度:19.6 FPS(batch size=1, 单张图片处理消耗时间≈0.051ms,没做任何加速处理)(AMD 3600,GTX1060-3g, CUDA-10.0, CuDNN-7.5)
输入:608
推理速度:11.6 FPS(batch size=1, 单张图片处理消耗时间≈0.086ms,没做任何加速处理)(AMD 3600,GTX1060-3g, CUDA-10.0, CuDNN-7.5)
在COCO test-dev上获得的AP结果如下:
4.加入PAN->yolo_v3_plus
参考yolov5,在FPN之后,再使用PAN,涨点明显。
目前,在COCO val上获得的AP结果如下:
输入:416
(YOLOv3Plus vs YOLOv3SPP)
vs vs推理速度:18.5 FPS(batch size=1, 单张图片处理消耗时间≈0.054ms,没做任何加速处理)(AMD 3600,GTX1060-3g, CUDA-10.0, CuDNN-7.5)
输入:608
vs vs推理速度:11.1 FPS(batch size=1, 单张图片处理消耗时间≈0.09ms,没做任何加速处理)(AMD 3600,GTX1060-3g, CUDA-10.0, CuDNN-7.5)
对比可见,添加了PAN结构后,网络涨点明显。具体的内容我们会在下文中详细说一下的~
COCO test-dev结果待更新……
5.加入Mosaic 增强
暂未测试(没卡用了……)
从性能上来看,还不赖,和官方的比起来不算太逊色。如果单单看指标的话,反而没有我之前的直接resize成方块的做法高~引起这一差距的原因可能是多方面的吧,比如数据增强、anchor box聚类的细节、预处理手段的差异等,这些都会或多或少地带来或正或负的影响。具体是哪块带来了差异,我委实没办法做消融实验来分析论证了,小小遗憾吧~
代码链接:
这个项目我单独开了一个,没有合并到之前的yolov2_yolov3系列中去,链接如下:
yjh0410/yolov3-plus_PyTorchgithub.com关于这个项目,我会单独地较为详细全面地介绍一次,可以视为单独的一个章节~
稍微补充一句,我的目的还是以教程为主吧,所以我的代码看起来都比较通俗易懂,没有花里胡哨的东西在里面,可能因此难以入一些水平高的人的眼,对此,还请多多包涵和见谅,我只是个兴趣使然的普通人~
下面,开始正文!
1.数据预处理
深度学习这一块,最少不了的、最依赖的莫过于数据了,但即使同一批数据,我们若是采用不同的处理方式,那么网络学习的时候也会提取出不同的特征,从而也就直接影响到了模型的性能。
在我之前的实现中,我“偷懒”地直接将图片resize成方块,如下图所示:
图片来源:《少女前线》官网那么这种偷懒的方式带来的最直观的问题就是:比例畸变。若是原始图片的长宽比越大,这种畸变就越明显,那么,我们直观上就可以想得到模型在看到这种畸变的时候会有多“头疼”。
举个例子,现在有两张图片,一张是有点“方”的图片去resize成方块:
图片来源:极简壁纸另一张是很“长”的图片去resize成方块:
图片来源:极简壁纸很明显,第二张图在resize后,人物的畸变很大。随着喂进来的数据量的增加、数据的多样化,网络老是看到这样千奇百怪的畸变,它估计也会发懵。而这,在yolov1、v2和SSD中都是这么做的。
那么后来在yolov3的时候,作者考虑到这样“粗暴”的方式所带来的畸变影响,故而使用了一种可以保证图片中的每个bounding box长宽比不发生变化的方式,如下图所示:
step1:padding补零第一步就是在较短的那条边上去对称的补零,补成一个方形。然后将这张补好的图片再resize成方块:
step2:resize通过这两步,我们就避免了在resize成方块的时候,bounding box的长宽比发生畸变。当然,也可以先resize再补零,不过,此时的resize就不是resize成方块,而是先将图片的最长边720去resize成416,那么480那条较短的边,就跟着原始图片的长宽比去resize成相应的尺寸就好,具体来说:
原始图片的长宽比:720/480=1.5。
因此,480的边应该resize成:416/1.5=277.33...,然后取整就是277。故而,原始图片先resize成416x277的图片,再将277的边补成416即可:
另一种方式当然, 我这里取得数不太好,做了取整的一个取舍,但这很正常,我们大多数时候没办法保证输入进来的图片尺寸就那么合适,合适到我们resize的时候完全不需要取舍。像277.33333这样的取整277,个人认为影响是可以忽略不计的,大体上还是保住了长宽比。
对于上面的两种方式,我选择的是第一种,大概是因为我不想去算长宽比吧~嘿嘿~
对于这一块的代码,我也贴了出来:
# zero paddingif height > width:img_ = np.zeros([height, height, 3])delta_w = height - widthleft = delta_w // 2img_[:, left:left+width, :] = imgoffset = np.array([[ left / height, 0., left / height, 0.]])scale = np.array([[width / height, 1., width / height, 1.]])elif height < width:img_ = np.zeros([width, width, 3])delta_h = width - heighttop = delta_h // 2img_[top:top+height, :, :] = imgoffset = np.array([[0., top / width, 0., top / width]])scale = np.array([[1., height / width, 1., height / width]])else:img_ = imgscale = np.array([[1., 1., 1., 1.]])offset = np.zeros([1, 4])if len(target) == 0:target = np.zeros([1, 5])else:target = np.array(target)target[:, :4] = target[:, :4] * scale + offsetimg, boxes, labels = self.transform(img_, target[:, :4], target[:, 4])# to rgbimg = img[:, :, (2, 1, 0)]# img = img.transpose(2, 0, 1)target = np.hstack((boxes, np.expand_dims(labels, axis=1)))还是比较好理解的。
那么,图片resize完了,bounding box的参数也得跟着做改动。我们以上段代码为例,其中的scale和offset两个变量就是用来调整bbox的参数,具体来说:
这里,我们假定已经对bounding box做好了归一化:
其中,
是bbox的左上角坐标, 是bbox的右下角坐标。再假设,我们是在height这个维度上去补零(对应代码第二个判断条件):
首先,我们需要将bbox映射回到原始图片的尺度:
这里是逐元素相乘。
然后,既然我们是在height这个维度上补零,并且,我们很容易算出来上半部分补零的高度是
。显然,此时bbox 的四个参数就发生了如下的变化:
然后,我们再把这个修正好的参数归一化即可——注意,现在的图片已经被我们padding成方块了,也就是尺寸发生了变声,切不可再用原始图片的wh去归一化,而是要用此时的尺寸去归一化,即:
因为w是最长边。
那么,把上面的过程综合起来,就和代码里的公式对应起来了。下面提供了两张在voc上用上面方法处理得到的416x416的图片。
图片来源:VOC2007图片来源:VOC2007到此,预处理这一块就说完了,剩下的就是再去做数据增强就好,数据增强和我以前的办法是一样的,没有变化。下一节中,我将详细说一下多尺度训练的操作。
2.多尺度训练
多尺度训练的方法很简单,首先我们先将所有的图片都预处理成640x640,然后再统一resize到当前需要训练的图片尺寸即可。
# multi-scale trick if iter_i % 10 == 0 and iter_i > 0 and args.multi_scale:# randomly choose a new sizesize = random.randint(10, 19) * 32input_size = [size, size]model.set_grid(input_size) if args.multi_scale:# interpolateimages = torch.nn.functional.interpolate(images, size=input_size, mode='bilinear', align_corners=False)这种操作是借鉴了U佬的pytorch版yolo的多尺度方法。这样,就可以使用多个workers来加快数据预处理过程,从而加快训练速度。
3.增加PAN结构
最近服务器大部分时间都蛮空闲的,所以就又在之前工作的基础上,添加了PAN,YOLOv3+SPP+PAN,想了想,就把这个网络命名为YOLOv3Plus。
PAN的使用我是借鉴了YOLOv5,关于yolov5的网络结构,这里我推荐一个很不错的文章:
江大白:深入浅出Yolo系列之Yolov5核心基础知识完整讲解zhuanlan.zhihu.com内容详实,把很多东西都讲了出来。尤其是里面画的网络结构图,太赞了,这里必须得给作者点个赞,太强了!
这里我就直接贴我的网络结构的代码吧~
# backbone darknet-53 (optional: darknet-19)self.backbone = darknet53(pretrained=trainable, hr=hr)# SPPself.spp = nn.Sequential(Conv(1024, 512, k=1),SPP(),BottleneckCSP(512*4, 1024, n=1, shortcut=False))# headself.head_conv_0 = Conv(1024, 512, k=1) # 10self.head_upsample_0 = UpSample(scale_factor=2)self.head_csp_0 = BottleneckCSP(512 + 512, 512, n=3,shortcut=False)# P3/8-smallself.head_conv_1 = Conv(512, 256, k=1) # 14self.head_upsample_1 = UpSample(scale_factor=2)self.head_csp_1 = BottleneckCSP(256 + 256, 256, n=3, shortcut=False)# P4/16-mediumself.head_conv_2 = Conv(256, 256, k=3, p=1, s=2)self.head_csp_2 = BottleneckCSP(256 + 256, 512, n=3, shortcut=False)# P8/32-largeself.head_conv_3 = Conv(512, 512, k=3, p=1, s=2)self.head_csp_3 = BottleneckCSP(512 + 512, 1024, n=3, shortcut=False)# det convself.head_det_1 = nn.Conv2d(256, self.anchor_number * (1 + self.num_classes + 4), 1)self.head_det_2 = nn.Conv2d(512, self.anchor_number * (1 + self.num_classes + 4), 1)self.head_det_3 = nn.Conv2d(1024, self.anchor_number * (1 + self.num_classes + 4), 1)backbone这一块,由于没有时间去在imagenet上训练我自己写的CSPDarknet53,所以就继续用原先的darknet53了,日后有时间完成了CSPDarknet53的预训练后,会回来继续更新的,到时候换个新的backbone后,毋庸置疑,还会再涨点的。
neck这一块,依旧是SPP,不同于之前的YOLOv3SPP,直接接个SPP,而是类似于yolov5那样,先对feature map做一下压缩,从1024channel压缩到512,然后再用SPP去处理,得到512x4=2048channel的feature map,接着用一层BottleneckCSP将channel压缩到1024。这样做的目的可能主要是为了更好地提取特征吧(总觉的不好解释的做法都可以通过想象力+更好的提取特征的办法来解释,哈哈哈哈~玩笑话了~)。
head就完全是照搬yolov5了,其中使用了BottleneckCSP结构,其代码我是直接使用了v5的源码:
# Copy from yolov5 class Bottleneck(nn.Module):# Standard bottleneckdef __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansionsuper(Bottleneck, self).__init__()c_ = int(c2 * e) # hidden channelsself.cv1 = Conv(c1, c_, k=1)self.cv2 = Conv(c_, c2, k=3, p=1, g=g)self.add = shortcut and c1 == c2def forward(self, x):return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))# Copy from yolov5 class BottleneckCSP(nn.Module):# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworksdef __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansionsuper(BottleneckCSP, self).__init__()c_ = int(c2 * e) # hidden channelsself.cv1 = Conv(c1, c_, k=1)self.cv2 = nn.Conv2d(c1, c_, kernel_size=1, bias=False)self.cv3 = nn.Conv2d(c_, c_, kernel_size=1, bias=False)self.cv4 = Conv(2 * c_, c2, k=1)self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3)self.act = nn.LeakyReLU(0.1, inplace=True)self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])def forward(self, x):y1 = self.cv3(self.m(self.cv1(x)))y2 = self.cv2(x)return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))又一次厚脸皮地做了个裁缝~
最后就是PAN了,在此之前,先做FPN,即将stride=32的feature map一直融合到stride=8(上采样用的是nearest,和v5一样,而不再是bilinear):
# backbonec3, c4, c5 = self.backbone(x)# neckc5 = self.spp(c5)# FPN + PAN# headc6 = self.head_conv_0(c5)c7 = self.head_upsample_0(c6) # s32->s16c8 = torch.cat([c7, c4], dim=1)c9 = self.head_csp_0(c8)# P3/8c10 = self.head_conv_1(c9)c11 = self.head_upsample_1(c10) # s16->s8c12 = torch.cat([c11, c3], dim=1)c13 = self.head_csp_1(c12) # to det然后,就是PAN:
# p4/16c14 = self.head_conv_2(c13)c15 = torch.cat([c14, c10], dim=1)c16 = self.head_csp_2(c15) # to det# p5/32c17 = self.head_conv_3(c16)c18 = torch.cat([c17, c6], dim=1)c19 = self.head_csp_3(c18) # to det最后,我们就得到了用于detection的三个尺度的feature map:c13、c16、c19,分别对应stride=8(小物体)、stride=16(中物体)、stride=32(大物体)。
代码逻辑很清楚,也不复杂,相信大家应该都能看明白FPN+PAN的结构和数据流动的过程。
4.Mosaic增强
在yolov4和yolov5中,都使用了Mosaic这一数据增强手段(据说U版的pytorch-yolov3中也用到了)。足以表明这一方式的确可以有效提升模型的性能。关于Mosaic增强,我就不做过多的解释了,知乎上已经有许多关于这一技术的介绍,如下面两篇:
等夏的初:YOLOv5从入门到部署之:数据读取与扩增zhuanlan.zhihu.com码农的后花园:YoloV4当中的Mosaic数据增强方法(附代码详细讲解)zhuanlan.zhihu.com以及下面这个回答(提出了和v4中的mosaic增强相似的技术):
如何评价新出的YOLO v4 ?www.zhihu.com因此,顺着这波趋势,我也试着加入了Mosaic这一新颖的数据增强手段。
不过,并没打算挖掘太深,所以,只实现了很朴素的一个mosaic增强版本,包括三步:
step1:随机获得四张图片,每张图片都经过标准的数据增强(如随机剪裁、色彩空间变换等)处理,最后都resize成同一大小,如416x416(再次强调,这里用zero padding的方式保住长宽比)。
step2:将四张图拼到一块,得到一个832x832的图片,然后将这一拼接好的图片再resize到416x416。
step3:将来自四张图片的targets做一下处理。
那么用代码来实现的话,以COCO数据集为例,相应的代码如下所示:
# mosaic augmentationif self.mosaic and np.random.randint(2):ids_list_ = self.ids[:index] + self.ids[index+1:]# random sample 3 indexsid2, id3, id4 = random.sample(ids_list_, 3)ids = [id2, id3, id4]img_lists = [img]tg_lists = [target]# load other 3 images and targetsfor id_ in ids:anno_ids = self.coco.getAnnIds(imgIds=[int(id_)], iscrowd=None)annotations = self.coco.loadAnns(anno_ids)# load image and preprocessimg_file = os.path.join(self.data_dir, self.name,'{:012}'.format(id_) + '.jpg')img_ = cv2.imread(img_file)if self.json_file == 'instances_val5k.json' and img_ is None:img_file = os.path.join(self.data_dir, 'train2017','{:012}'.format(id_) + '.jpg')img_ = cv2.imread(img_file)assert img_ is not Noneheight_, width_, channels_ = img_.shape # COCOAnnotation Transform# start here :target_ = []for anno in annotations:x1 = np.max((0, anno['bbox'][0]))y1 = np.max((0, anno['bbox'][1]))x2 = np.min((width_ - 1, x1 + np.max((0, anno['bbox'][2] - 1))))y2 = np.min((height_ - 1, y1 + np.max((0, anno['bbox'][3] - 1))))if anno['area'] > 0 and x2 >= x1 and y2 >= y1:label_ind = anno['category_id']cls_id = self.class_ids.index(label_ind)x1 /= width_y1 /= height_x2 /= width_y2 /= height_target_.append([x1, y1, x2, y2, cls_id]) # [xmin, ymin, xmax, ymax, label_ind]# end here .img_lists.append(img_)tg_lists.append(target_)# preprocessimg_processed_lists = []tg_processed_lists = []for img, target in zip(img_lists, tg_lists):h, w, _ = img.shapeimg_, scale, offset = self.preprocess(img, target, h, w)if len(target) == 0:target = np.zeros([1, 5])else:target = np.array(target)target[:, :4] = target[:, :4] * scale + offset# augmentationimg, boxes, labels = self.transform(img_, target[:, :4], target[:, 4])# to rgbimg = img[:, :, (2, 1, 0)]# img = img.transpose(2, 0, 1)target = np.hstack((boxes, np.expand_dims(labels, axis=1)))img_processed_lists.append(img)tg_processed_lists.append(target)# Then, we use mosaic augmentationimg_size = self.transform.size[0]mosaic_img = np.zeros([img_size*2, img_size*2, 3])img_1, img_2, img_3, img_4 = img_processed_liststg_1, tg_2, tg_3, tg_4 = tg_processed_lists# stitch imagesmosaic_img[:img_size, :img_size] = img_1mosaic_img[:img_size, img_size:] = img_2mosaic_img[img_size:, :img_size] = img_3mosaic_img[img_size:, img_size:] = img_4mosaic_img = cv2.resize(mosaic_img, (img_size, img_size))# modify targetstg_1[:, :4] /= 2.0tg_2[:, :4] = (tg_2[:, :4] + np.array([1., 0., 1., 0.])) / 2.0tg_3[:, :4] = (tg_3[:, :4] + np.array([0., 1., 0., 1.])) / 2.0tg_4[:, :4] = (tg_4[:, :4] + 1.0) / 2.0target = np.concatenate([tg_1, tg_2, tg_3, tg_4], axis=0)return torch.from_numpy(mosaic_img).permute(2, 0, 1).float(), target, height, width, offset, scale代码的逻辑很清晰,没有拐弯抹角的地方。下图展示了一个经过我这种很朴素的mosaic增强的例子:
显然啦,和v4、v5不一样。权当作一次小练习就好了~有时间的话,我会试着实现一版更好的mosaic增强。
mosaic增强手段的一个显著优点就是会增加小目标的数量,再配合多尺度训练,在COCO数据集上势必会有不小的提升,至于会带来多大的提升,还得等后续的实测了(如果我能用上服务器的话……)
5.其他的trick
CIoU是不是也可以试一试呢~
总结
以上是生活随笔为你收集整理的yolov3 推理所需要的时间_目标检测-番外五:YOLOv3-Plus的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: arduino下载库出错_arduino
- 下一篇: 如何在柱状图中点连线_练瑜伽,如何放松僵