目標檢測-基於Pytorch實現Yolov3(1)- 搭建模型
原文地址: ofollow,noindex">https://www.cnblogs.com/jacklu/p/9853599.html
本人前段時間在T廠做了目標檢測的專案,對一些目標檢測框架也有了一定理解。其中Yolov3速度非常快,效果也還可以,但在github上還沒有完整的基於pytorch的yolov3程式碼,目前star最多的pytorch yolov3專案只能做預測,沒有訓練程式碼,而且我看了它的model寫得不是很有層次。自己準備利用接下來的幾個週末把這個坑填上。
希望能夠幫助開發者瞭解如何基於Pytorch實現一個強大的目標檢測模型,同時可以方便的將模型應用到自己的資料集裡。完整的原始碼準備在文章結束後考慮釋出在github上。
準備的目錄:
目標檢測-基於Pytorch實現Yolov3(1)- 搭建模型 (model.py,最容易的部分,所以第一篇寫這個)
目標檢測-基於Pytorch實現Yolov3(2)- 資料預處理及資料載入 (dataloader.py,非常重要的一部分,程式碼工作量最大,定製化只要在這一部分下功夫)
目標檢測-基於Pytorch實現Yolov3(3)- 目標函式 (loss.py,最重要的部分,直接決定了網路的效果,難度也是5部分裡最大的)
目標檢測-基於Pytorch實現Yolov3(4)- 模型訓練 (train.py,前面重要的3部分都做完了,這部分就是寫完程式碼喝茶看曲線的時間)
目標檢測-基於Pytorch實現Yolov3(5)- 模型預測 (test.py,檢驗模型訓練好壞,還有一些坑要填)
程式碼主要參考github上基於keras的yolov3的實現,程式碼結構非常清晰。
Darknet卷積模組
Yolo系列的作者把yolo網路叫做Darknet,其實其他神經網路庫都已經把卷積層寫好了,直接堆疊起來即可。
darknet卷積模組是這個模型裡最基本的網路單元,包括卷積層、batch norm(BN)層、啟用函式,因此型別命名為 DarknetConv2D_BN_Leaky。原keras實現是卷積層加了L2正則化預防過擬合,Pytorch是把這個操作放到了Optimizer中,所以將在第三部分講解。
用Pytorch需要注意, 如果你訓練的時候GPU視訊記憶體不大,batch size設的很小,這時候你就要考慮訓練資料集的分佈情況。舉個例子,加入你的batch size設成了1,但你資料每張圖差別都很大,這會導致你的網路一直在震盪,即使網路能夠訓練到很低的training loss,
在做預測的時候效果也不好,這主要是BN造成的。因為每批資料的統計量(均值和方差)都不同,而且差別大,這就導致網路訓練學不到好的BN層的統計量。如果直接去掉BN層,你會發現網路訓練非常慢,所以BN層還是要加的,好在Pytorch裡的BN有個介面來控制要不要記住每批訓練的統計量,即 track_running_stats=True ,如果訓練的batch size不能設特別大,就把它改成False。
卷積層、BN層說完了,啟用函式Yolo裡用的是0.1的LeakReLU,本人實驗發現和ReLU沒什麼明顯的區別(水論文真是一門藝術,我的水文怎麼就不中嘞?)
結構很簡答,這部分直接上程式碼,不畫圖了。
import torch.nn as nn import torch class DarknetConv2D_BN_Leaky(nn.Module): def __init__(self, numIn, numOut, ksize, stride = 1, padding = 1): super(DarknetConv2D_BN_Leaky, self).__init__() self.conv1 = nn.Conv2d(numIn, numOut, ksize, stride, padding)#regularizer': l2(5e-4) self.bn1 = nn.BatchNorm2d(numOut) self.leakyReLU = nn.LeakyReLU(0.1) def forward(self, x): x = self.conv1(x) x = self.bn1(x) x = self.leakyReLU(x) return x
殘差模組
殘差模組是借鑑了ResNet,殘差模組是為了保證深的模型能夠得到很好的訓練。殘差模組ResidualBlock,對外介面有numIn, numOut, numBlock,分別控制模組的輸入通道數,輸出通道數(卷積核數)和殘差模組的堆疊次數。下圖是一個numBlock = 2 的模型,注意這裡CONV是指上一部分說的Darknet卷積模組,第一個模組(D2)表示是這個卷積模組stride = 2,及順便執行了2倍降取樣操作。也就是說特徵每經過一個殘差模組,解析度降為原來的一半。
class ResidualBlock(nn.Module): def __init__(self, numIn, numOut, numBlock): super(ResidualBlock, self).__init__() self.numBlock = numBlock self.dark_conv1 = DarknetConv2D_BN_Leaky(numIn, numOut, ksize = 3, stride = 2, padding = 1) self.dark_conv2 = [] for i in range(self.numBlock): layers = [] layers.append(DarknetConv2D_BN_Leaky(numOut, numOut//2, ksize = 1, stride = 1, padding = 0)) layers.append(DarknetConv2D_BN_Leaky(numOut//2, numOut, ksize = 3, stride = 1, padding = 1)) self.dark_conv2.append(nn.Sequential(*layers)) self.dark_conv2 = nn.ModuleList(self.dark_conv2) def forward(self, x): x = self.dark_conv1(x) for convblock in self.dark_conv2: residual = x x = self.convblock(x) x = x + residual return x
後端輸出模組
後端輸出模組是一個三次降取樣(三次升取樣在下一部分介紹),這三次降取樣+三次升取樣,類似Encoder-Decoder的FCN模型。是為了在三種不同尺度上預測。本系列將在voc2007上訓練,訓練前輸入圖片要resize到256x256,那麼這三種尺度分別是32x32,16x16,8x8。這一部分是因為圖片中的目標有大有小,為了保證從不同尺度上找到最好尺度的特徵圖來進行預測。當然準確提升的同時,由於解析度有提升,計算量又有一定的增加,索性我們這裡的解析度不大。下圖所示為最後輸出模組,這個模組有兩個輸出,一個是用作下一個模組的輸入,一個是用於輸出目標檢測結果,即座標、類別和目標置信度,這一部分將在下一篇詳細介紹。注意紅色的Conv不是DarknetConv2D_BN_Leaky,而是指普通的卷積模組。
class LastLayer(nn.Module): def __init__(self, numIn, numOut, numOut2): super(LastLayer, self).__init__() self.dark_conv1 = DarknetConv2D_BN_Leaky(numIn, numOut, ksize = 1, stride = 1, padding = 0) self.dark_conv2 = DarknetConv2D_BN_Leaky(numOut, numOut*2, ksize = 3, stride = 1, padding = 1) self.dark_conv3 = DarknetConv2D_BN_Leaky(numOut*2, numOut, ksize = 1, stride = 1, padding = 0) self.dark_conv4 = DarknetConv2D_BN_Leaky(numOut, numOut*2, ksize = 3, stride = 1, padding = 1) self.dark_conv5 = DarknetConv2D_BN_Leaky(numOut*2, numOut, ksize = 1, stride = 1, padding = 0) self.dark_conv6 = DarknetConv2D_BN_Leaky(numOut, numOut*2, ksize = 3, stride = 1, padding = 1) self.conv7 = nn.Conv2d(numOut*2, numOut2, 1, stride = 1, padding = 0) def forward(self, x): x = self.dark_conv1(x) x = self.dark_conv2(x) x = self.dark_conv3(x) x = self.dark_conv4(x) x = self.dark_conv5(x) y = self.dark_conv6(x) y = self.conv7(y) return x,y
Yolov3模型
基本的模組已經定義好,Yolov3的模型就是把這些模型疊加起來。注意下圖就是Yolov3的簡化模型,數字表示該上一個模組的輸出特徵尺寸(CxHxW),相應的顏色對應相應的模組
class Yolov3(nn.Module): def __init__(self, numAnchor, numClass): super(Yolov3, self).__init__() self.dark_conv1 = DarknetConv2D_BN_Leaky(3, 32, ksize = 3, stride = 1, padding = 1) self.res1 = ResidualBlock(32, 64, 1) self.res2 = ResidualBlock(64, 128, 2) self.res3 = ResidualBlock(128, 256, 8) self.res4 = ResidualBlock(256, 512, 8) self.res5 = ResidualBlock(512, 1024, 4) self.last1 = LastLayer(1024, 512, numAnchor*(numClass+5)) self.up1 = nn.Sequential(DarknetConv2D_BN_Leaky(512, 256, ksize = 1, stride = 1, padding = 0), nn.Upsample(scale_factor=2)) self.last2 = LastLayer(768, 256, numAnchor*(numClass+5)) self.up2 = nn.Sequential(DarknetConv2D_BN_Leaky(256, 128, ksize = 1, stride = 1, padding = 0), nn.Upsample(scale_factor=2)) self.last3 = LastLayer(384, 128, numAnchor*(numClass+5)) def forward(self, x): x = self.dark_conv1(x)#32x256x256 x = self.res1(x)#64x128x128 x = self.res2(x)#128x64x64 x3 = self.res3(x)#256x32x32 x4 = self.res4(x3)#512x16x16 x5 = self.res5(x4)#1024x8x8 x,y1 = self.last1(x5)#512x8x8, x = self.up1(x)#256x16x16 x = torch.cat((x, x4), 1)#768x16x16 x,y2 = self.last2(x)#256x16x16 x = self.up2(x)#128x32x32 x = torch.cat((x, x3), 1)#384x32x32 x,y3 = self.last3(x)#128x32x32 return y1,y2,y3
到這裡模型已經完成,模型程式碼結構非常清晰。有人可能會問,為什麼要這種堆疊方式,其實我自己也覺得模型沒什麼特別的地方,自己根據新的需求定義網路結構完全可以,但是要注意模型深度增加時如何保證收斂,如何加速模型訓練,同時輸出特徵的解析度要計算好。
參考資料
Yolov3 論文:https://pjreddie.com/media/files/papers/YOLOv3.pdf
Yolov3 Keras實現:https://github.com/qqwweee/keras-yolo3