這是關於從頭實現YOLO v3檢測器的第3部分。在上一部分中,我們實現了YOLO架構中使用的圖層,在這一部分中,我們將在PyTorch中實現YOLO的網絡架構,這樣我們就可以產生一個給定圖片的輸出。
我們的目標是設計網絡的前向傳播。
本教程設計的代碼在Python 3.5和PyTorch 0.4上運行。完整代碼可以在這裏找到 Github repo.
本教程分爲5個部分:
- Part 1 : Understanding How YOLO works
- Part 2 : Creating the layers of the network architecture
- Part 3 (This one): Implementing the the forward pass of the network
- Part 4 : Objectness Confidence Thresholding and Non-maximum Suppression
- Part 5 : Designing the input and the output pipelines
預備知識
- 課程Part 1 和 Part 2 。
- 基本的PyTorch工作知識,包括如何使用nn.Module、nn.sequence和torch.nn.parameter類創建自定義架構。
- 在PyTorch中處理圖片
定義網絡
正如我前面指出的,我們使用nn.Module類在PyTorch中構建自定義架構。讓我們爲檢測器定義一個網絡。在darknet.py文件中,我們添加了以下類。
class Darknet(nn.Module):
def __init__(self, cfgfile):
super(Darknet, self).__init__()
self.blocks = parse_cfg(cfgfile)
self.net_info, self.module_list = create_modules(self.blocks)
在這裏,我們對nn.Module類進行了子類化,並將類命名爲Darknet。我們使用成員 blocks, net_info 和 module_list初始化網絡。
實現網絡前向傳播
通過重寫 nn.Module 類的前向傳播方法,實現網絡的前向傳播.
forward 有兩個目的。首先,計算輸出,然後,以一種更容易處理的方式轉換輸出檢測特徵映射(比如將它們轉換爲跨多個尺度的檢測映射,這樣就可以將它們連接起來,否則就不可能實現,因爲它們具有不同的維度)。
def forward(self, x, CUDA):
modules = self.blocks[1:]
outputs = {} #We cache the outputs for the route layer
forward 需要三個參數,self,輸入x和CUDA,哪一個爲真,將使用GPU來加速前向傳播。
在這裏,我們迭代self.blocks[1:]而不是self.blocks,因爲self.blocks的第一個元素是一個不屬於前向傳播的net塊。
由於 route 和 shortcut需要來自前一層的輸出映射,我們將每個層的輸出特徵映射緩存到一個dictoutput中。鍵是層的索引,值是特徵映射。
與create_modules函數一樣,現在我們遍歷包含網絡模塊的module_list。這裏需要注意的是,添加模塊的順序與配置文件中的順序相同。這意味着,我們可以簡單地通過每個模塊運行輸入來獲得輸出。
write = 0 #This is explained a bit later
for i, module in enumerate(modules):
module_type = (module["type"])
Convolutional and Upsample Layers
如果模塊是卷積模塊或upsample模塊,這就是向前傳播的工作方式。
if module_type == "convolutional" or module_type == "upsample":
x = self.module_list[i](x)
Route Layer / Shortcut Layer
如果您查看路由層的代碼,我們必須考慮兩種情況(如第2部分所述)。對於必須連接兩個feature map的情況,我們使用torch.cat函數,第二個參數爲1。這是因爲我們想要沿着深度連接特徵映射。(在PyTorch中,卷積層的輸入和輸出的格式爲B×C×H×w,深度與通道維數對應)。
elif module_type == "route":
layers = module["layers"]
layers = [int(a) for a in layers]
if (layers[0]) > 0:
layers[0] = layers[0] - i
if len(layers) == 1:
x = outputs[i + (layers[0])]
else:
if (layers[1]) > 0:
layers[1] = layers[1] - i
map1 = outputs[i + layers[0]]
map2 = outputs[i + layers[1]]
x = torch.cat((map1, map2), 1)
elif module_type == "shortcut":
from_ = int(module["from"])
x = outputs[i-1] + outputs[i+from_]
YOLO (Detection Layer)
YOLO的輸出是一個卷積feature map,它包含沿feature map深度的邊框屬性。單元格預測的屬性邊界框是一個接一個堆疊在一起的。因此,如果必須訪問單元格的第二個邊界(5,6),則必須通過map[5,6,(5+C): 2*(5+C)]對其進行索引。這種格式對於對象置信度的閾值化、向中心添加網格偏移量、應用錨點等輸出處理非常不方便。
另一個問題是,由於探測發生在三個尺度上,所以預測圖的維數會有所不同。雖然這三個feature map的尺寸不同,但是要對它們執行的輸出處理操作是相似的。在一個張量上做這些運算會更好,而不是在三個單獨的張量上。
爲了解決這些問題,我們引入了函數predict_transform。
Transforming the output
函數predict_transform位於文件util.py中,當我們在Darknet類的forward中使用它時,將導入該函數。
在util.py的頂部添加導入。
from __future__ import division
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
import cv2
predict_transform 取5個參數; prediction (輸出), inp_dim (輸入圖像維度), anchors, num_classes, 和一個可選的CUDA標誌。
def predict_transform(prediction, inp_dim, anchors, num_classes, CUDA = True):
predict_transform函數採用一個檢測特徵映射,並將其轉換爲一個二維張量,其中張量的每一行對應一個包圍框的屬性,順序如下:
下面是執行上述轉換的代碼。
batch_size = prediction.size(0)
stride = inp_dim // prediction.size(2)
grid_size = inp_dim // stride
bbox_attrs = 5 + num_classes
num_anchors = len(anchors)
prediction = prediction.view(batch_size, bbox_attrs*num_anchors, grid_size*grid_size)
prediction = prediction.transpose(1,2).contiguous()
prediction = prediction.view(batch_size, grid_size*grid_size*num_anchors, bbox_attrs)
錨的尺寸與網塊的高度和寬度屬性一致。這些屬性描述了輸入圖像的尺寸,它比檢測圖大(以步幅的係數計算)。
因此,我們必須用檢測特徵圖的步幅來劃分錨點。
anchors = [(a[0]/stride, a[1]/stride) for a in anchors]
現在,我們需要根據第1部分中討論的方程轉換輸出。
用Sigmoid 處理x、y座標和目標得分。
#Sigmoid the centre_X, centre_Y. and object confidencce
prediction[:,:,0] = torch.sigmoid(prediction[:,:,0])
prediction[:,:,1] = torch.sigmoid(prediction[:,:,1])
prediction[:,:,4] = torch.sigmoid(prediction[:,:,4])
將網格偏移量添加到中心座標預測中。
#Add the center offsets
grid = np.arange(grid_size)
a,b = np.meshgrid(grid, grid)
x_offset = torch.FloatTensor(a).view(-1,1)
y_offset = torch.FloatTensor(b).view(-1,1)
if CUDA:
x_offset = x_offset.cuda()
y_offset = y_offset.cuda()
x_y_offset = torch.cat((x_offset, y_offset), 1).repeat(1,num_anchors).view(-1,2).unsqueeze(0)
prediction[:,:,:2] += x_y_offset
將錨點應用於邊界框的尺寸
#log space transform height and the width
anchors = torch.FloatTensor(anchors)
if CUDA:
anchors = anchors.cuda()
anchors = anchors.repeat(grid_size*grid_size, 1).unsqueeze(0)
prediction[:,:,2:4] = torch.exp(prediction[:,:,2:4])*anchors
將sigmoid激活應用於類得分
prediction[:,:,5: 5 + num_classes] = torch.sigmoid((prediction[:,:, 5 : 5 + num_classes]))
我們在這裏要做的最後一件事是將檢測圖的大小調整爲輸入圖像的大小。 此處的邊界框屬性根據feature map(例如,13 x 13)調整大小。 如果輸入圖像是416 x 416,我們將屬性乘以32或stride變量。
prediction[:,:,:4] *= stride
到此循環體結束。
返回函數末尾的預測。
return prediction
Detection Layer Revisited
現在我們已經轉換了輸出張量,現在我們可以將三種不同尺度的檢測圖連接成一個大張量。 請注意,在轉換之前這是不可能的,因爲無法連接具有不同空間維度的feature map。 但是從現在開始,我們的輸出張量就像一個表格一樣,用包圍框作爲它的行,連接是非常可能的。
我們的一個障礙是我們不能初始化一個空張量,然後將一個非空(不同形狀)張量連接到它上面。 因此,我們延遲收集器的初始化(包含檢測的張量),直到我們得到第一個檢測圖,然後在我們得到後續檢測時連接到它的映射。
注意函數forward循環之前的write = 0行。write標誌用於指示我們是否遇到了第一次檢測。如果write爲0,則表示收集器尚未初始化,如果它是1,就意味着收集器已經初始化,我們可以將檢測映射連接到它。
現在,我們已經用predict_transform函數武裝了自己,我們在forward函數中編寫了用於處理檢測特徵映射的代碼。
在darknet.py文件的頂部,添加以下導入。
from util import *
然後,在forward函數中
elif module_type == 'yolo':
anchors = self.module_list[i][0].anchors
#Get the input dimensions
inp_dim = int (self.net_info["height"])
#Get the number of classes
num_classes = int (module["classes"])
#Transform
x = x.data
x = predict_transform(x, inp_dim, anchors, num_classes, CUDA)
if not write: #if no collector has been intialised.
detections = x
write = 1
else:
detections = torch.cat((detections, x), 1)
outputs[i] = x
現在,只需返回檢測。
return detections
測試前向傳播
這是一個創建虛擬輸入的函數。 我們會將此輸入傳遞給我們的網絡。 在編寫此函數之前,請將此圖像 image 保存到工作目錄中。 如果您使用的是Linux,請輸入。
wget https://github.com/ayooshkathuria/pytorch-yolo-v3/raw/master/dog-cycle-car.png
現在,在你的darknet.py文件頂部定義如下函數:
def get_test_input():
img = cv2.imread("dog-cycle-car.png")
img = cv2.resize(img, (416,416)) #大小調整爲輸入維度
img_ = img[:,:,::-1].transpose((2,0,1)) # BGR -> RGB | H X W C -> C X H X W
img_ = img_[np.newaxis,:,:,:]/255.0 #Add a channel at 0 (for batch) | Normalise
img_ = torch.from_numpy(img_).float() #轉換爲浮點數
img_ = Variable(img_) #轉換爲變量
return img_
然後,我們輸入如下代碼:
model = Darknet("cfg/yolov3.cfg")
inp = get_test_input()
pred = model(inp, torch.cuda.is_available())
print (pred)
你會看到如下輸出。
( 0 ,.,.) =
16.0962 17.0541 91.5104 ... 0.4336 0.4692 0.5279
15.1363 15.2568 166.0840 ... 0.5561 0.5414 0.5318
14.4763 18.5405 409.4371 ... 0.5908 0.5353 0.4979
⋱ ...
411.2625 412.0660 9.0127 ... 0.5054 0.4662 0.5043
412.1762 412.4936 16.0449 ... 0.4815 0.4979 0.4582
412.1629 411.4338 34.9027 ... 0.4306 0.5462 0.4138
[torch.FloatTensor of size 1x10647x85]
這個張量的形狀是1 x 10647 x 85。第一個維度是批處理大小,簡單來說就是1,因爲我們使用了單個圖像。對於批處理中的每個圖像,我們都有一個10647 x 85的表。該表的每一行表示一個邊界框。(4個bbox屬性,1個對象分數和80個類分數)
此時,我們的網絡具有隨機權值,不會產生正確的輸出。我們需要在網絡中加載一個權重文件。爲此,我們將使用官方權重文件。
下載預訓練權重
將權重文件下載到檢測器目錄中。從這裏 here.獲取權重文件。如果你使用linux。
wget https://pjreddie.com/media/files/yolov3.weights
理解權重文件
官方權重文件是二進制文件,它包含以串行方式存儲的權重。
必須極其小心地讀出權重。權重只是作爲浮點數存儲,沒有任何東西可以指導我們它們屬於哪個層。如果你搞砸了沒有什麼能阻止你,比如說,將批處理規範層的權值加載到卷積層的權值中。因爲您只讀取浮點數,所以無法區分哪個權重屬於哪個層。因此,我們必須理解權重的存儲方式。
首先,權值只屬於兩種類型的層,一種是批處理規範層,另一種是卷積層。
這些層的權重存儲的順序與它們在配置文件中出現的順序完全相同。因此,如果一個 convolutional後面跟着一個 shortcut ,然後這個 shortcut 後面跟着另一個convolutional,那麼文件應該包含前一個convolutional 的權重,然後是後一個卷積塊的權重。
當批處理規範層出現在 convolutional中時,不存在偏差。但是,當沒有批處理規範層時,偏差“權重”必須從文件中讀取。
下圖總結了權重如何存儲權重。
下載權重
讓我們編寫一個函數加載權重。 它將成爲Darknet類的成員函數。 除了self之外,還需要一個參數,即權重文件的路徑。
def load_weights(self, weightfile):
權重文件的前160個字節存儲5個int32值,這些值構成文件頭。
#Open the weights file
fp = open(weightfile, "rb")
#The first 5 values are header information
# 1. Major version number
# 2. Minor Version Number
# 3. Subversion number
# 4,5. Images seen by the network (during training)
header = np.fromfile(fp, dtype = np.int32, count = 5)
self.header = torch.from_numpy(header)
self.seen = self.header[3]
其餘的位現在按上面描述的順序表示權重。權重存儲爲float32或32位浮點數。讓我們在np.ndarray中加載其餘的權重。
weights = np.fromfile(fp, dtype = np.float32)
現在,我們迭代權重文件,並將權重加載到網絡模塊中。
ptr = 0
for i in range(len(self.module_list)):
module_type = self.blocks[i + 1]["type"]
#If module_type is convolutional load weights
#Otherwise ignore.
在循環中,我們首先檢查卷積塊batch_normalize是否爲真。在此基礎上,我們加載權重。
if module_type == "convolutional":
model = self.module_list[i]
try:
batch_normalize = int(self.blocks[i+1]["batch_normalize"])
except:
batch_normalize = 0
conv = model[0]
我們保留一個名爲ptr的變量來跟蹤我們在權重數組中的位置。現在,如果batch_normalize爲真,則按以下方式加載權重。
if (batch_normalize):
bn = model[1]
#Get the number of weights of Batch Norm Layer
num_bn_biases = bn.bias.numel()
#Load the weights
bn_biases = torch.from_numpy(weights[ptr:ptr + num_bn_biases])
ptr += num_bn_biases
bn_weights = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
bn_running_mean = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
bn_running_var = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
#Cast the loaded weights into dims of model weights.
bn_biases = bn_biases.view_as(bn.bias.data)
bn_weights = bn_weights.view_as(bn.weight.data)
bn_running_mean = bn_running_mean.view_as(bn.running_mean)
bn_running_var = bn_running_var.view_as(bn.running_var)
#Copy the data to model
bn.bias.data.copy_(bn_biases)
bn.weight.data.copy_(bn_weights)
bn.running_mean.copy_(bn_running_mean)
bn.running_var.copy_(bn_running_var)
如果batch_norm不爲真,則只需加載卷積層的偏差。
else:
#Number of biases
num_biases = conv.bias.numel()
#Load the weights
conv_biases = torch.from_numpy(weights[ptr: ptr + num_biases])
ptr = ptr + num_biases
#reshape the loaded weights according to the dims of the model weights
conv_biases = conv_biases.view_as(conv.bias.data)
#Finally copy the data
conv.bias.data.copy_(conv_biases)
最後,我們加載卷積層權重。
#Let us load the weights for the Convolutional layers
num_weights = conv.weight.numel()
#Do the same as above for weights
conv_weights = torch.from_numpy(weights[ptr:ptr+num_weights])
ptr = ptr + num_weights
conv_weights = conv_weights.view_as(conv.weight.data)
conv.weight.data.copy_(conv_weights)
我們已經完成了這個函數,現在您可以通過調用Darknet對象上的load_weights函數來在加載Darknet對象中的權重。
model = Darknet("cfg/yolov3.cfg")
model.load_weights("yolov3.weights")
以上就是本部分的全部內容,在構建了模型並加載了權重之後,我們終於可以開始檢測對象了。在下一部分中,我們將介紹如何使用對象置信度閾值和非最大抑制來生成最終的檢測集。
擴展閱讀
- PyTorch tutorial
- Reading binary files with NumPy
- nn.Module, nn.Parameter classes