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_PyTorch?github.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
- 下一篇: 如何在柱状图中点连线_练瑜伽,如何放松僵