摘要:全卷積神經網絡僅使用卷積層,這就使其成為全卷積神經網絡。輸入圖像中包含了真值對象框中心的網格會作為負責預測對象的單元格。在圖像中,它是被標記為紅色的單元格,其中包含了真值框的中心被標記為黃色。
在過去幾個月中,我一直在實驗室中研究提升目標檢測的方法。在這之中我獲得的較大啟發就是意識到:學習目標檢測的較佳方法就是自己動手實現這些算法,而這正是本教程引導你去做的。
在本教程中,我們將使用 PyTorch 實現基于 YOLO v3 的目標檢測器,后者是一種快速的目標檢測算法。
本教程使用的代碼需要運行在 Python 3.5 和 PyTorch 0.3 版本之上。你可以在以下鏈接中找到所有代碼:
https://github.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch?
本教程包含五個部分:
1. YOLO 的工作原理
2. 創建 YOLO 網絡層級
3. 實現網絡的前向傳播
4. objectness 置信度閾值和非極大值抑制
5. 設計輸入和輸出管道
所需背景知識
在學習本教程之前,你需要了解:
卷積神經網絡的工作原理,包括殘差塊、跳過連接和上采樣;
目標檢測、邊界框回歸、IoU 和非極大值抑制;
基礎的 PyTorch 使用。你需要能夠輕松地創建簡單的神經網絡。
什么是 YOLO?
YOLO 是 You Only Look Once 的縮寫。它是一種使用深度卷積神經網絡學得的特征來檢測對象的目標檢測器。在我們上手寫代碼之前,我們必須先了解 YOLO 的工作原理。
全卷積神經網絡
YOLO 僅使用卷積層,這就使其成為全卷積神經網絡(FCN)。它擁有 75 個卷積層,還有跳過連接和上采樣層。它不使用任何形式的池化,使用步幅為 2 的卷積層對特征圖進行下采樣。這有助于防止通常由池化導致的低級特征丟失。
作為 FCN,YOLO 對于輸入圖像的大小并不敏感。然而,在實踐中,我們可能想要持續不變的輸入大小,因為各種問題只有在我們實現算法時才會浮現出來。
這其中的一個重要問題是:如果我們希望按批次處理圖像(批量圖像由 GPU 并行處理,這樣可以提升速度),我們就需要固定所有圖像的高度和寬度。這就需要將多個圖像整合進一個大的批次(將許多 PyTorch 張量合并成一個)。
YOLO 通過被步幅對圖像進行上采樣。例如,如果網絡的步幅是 32,則大小為 416×416 的輸入圖像將產生 13×13 的輸出。通常,網絡層中的任意步幅都指層的輸入除以輸入。
解釋輸出
典型地(對于所有目標檢測器都是這種情況),卷積層所學習的特征會被傳遞到分類器/回歸器,從而進行預測(邊界框的坐標、類別標簽等)。
在 YOLO 中,預測是通過卷積層完成的(它是一個全卷積神經網絡,請記?。。┢浜诵某叽鐬椋?/p>
1×1×(B×(5+C))
現在,首先要注意的是我們的輸出是一個特征圖。由于我們使用了 1×1 的卷積,所以預測圖的大小恰好是之前特征圖的大小。在 YOLO v3(及其更新的版本)上,預測圖就是每個可以預測固定數量邊界框的單元格。
雖然形容特征圖中單元的正確術語應該是「神經元」,但本文中為了更為直觀,我們將其稱為單元格(cell)。
深度方面,特征圖中有 (B x (5 + C))* *個條目。B 代表每個單元可以預測的邊界框數量。根據 YOLO 的論文,這些 B 邊界框中的每一個都可能專門用于檢測某種對象。每個邊界框都有 5+C 個屬性,分別描述每個邊界框的中心坐標、維度、objectness 分數和 C 類置信度。YOLO v3 在每個單元中預測 3 個邊界框。
如果對象的中心位于單元格的感受野內,你會希望特征圖的每個單元格都可以通過其中一個邊界框預測對象。(感受野是輸入圖像對于單元格可見的區域。)
這與 YOLO 是如何訓練的有關,只有一個邊界框負責檢測任意給定對象。首先,我們必須確定這個邊界框屬于哪個單元格。
因此,我們需要切分輸入圖像,把它拆成維度等于最終特征圖的網格。
讓我們思考下面一個例子,其中輸入圖像大小是 416×416,網絡的步幅是 32。如之前所述,特征圖的維度會是 13×13。隨后,我們將輸入圖像分為 13×13 個網格。
輸入圖像中包含了真值對象框中心的網格會作為負責預測對象的單元格。在圖像中,它是被標記為紅色的單元格,其中包含了真值框的中心(被標記為黃色)。
現在,紅色單元格是網格中第七行的第七個。我們現在使特征圖中第七行第七個單元格(特征圖中的對應單元格)作為檢測狗的單元。
現在,這個單元格可以預測三個邊界框。哪個將會分配給狗的真值標簽?為了理解這一點,我們必須理解錨點的概念。
請注意,我們在這里討論的單元格是預測特征圖上的單元格,我們將輸入圖像分隔成網格,以確定預測特征圖的哪個單元格負責預測對象。
錨點框(Anchor Box)
預測邊界框的寬度和高度看起來非常合理,但在實踐中,訓練會帶來不穩定的梯度。所以,現在大部分目標檢測器都是預測對數空間(log-space)變換,或者預測與預訓練默認邊界框(即錨點)之間的偏移。
然后,這些變換被應用到錨點框來獲得預測。YOLO v3 有三個錨點,所以每個單元格會預測 3 個邊界框。
回到前面的問題,負責檢測狗的邊界框的錨點有較高的 IoU,且有真值框。
預測
下面的公式描述了網絡輸出是如何轉換,以獲得邊界框預測結果的。
中心坐標
注意:我們使用 sigmoid 函數進行中心坐標預測。這使得輸出值在 0 和 1 之間。原因如下:
正常情況下,YOLO 不會預測邊界框中心的確切坐標。它預測:
與預測目標的網格單元左上角相關的偏移;
使用特征圖單元的維度(1)進行歸一化的偏移。
以我們的圖像為例。如果中心的預測是 (0.4, 0.7),則中心在 13 x 13 特征圖上的坐標是 (6.4, 6.7)(紅色單元的左上角坐標是 (6,6))。
但是,如果預測到的 x,y 坐標大于 1,比如 (1.2, 0.7)。那么中心坐標是 (7.2, 6.7)。注意該中心在紅色單元右側的單元中,或第 7 行的第 8 個單元。這打破了 YOLO 背后的理論,因為如果我們假設紅色框負責預測目標狗,那么狗的中心必須在紅色單元中,不應該在它旁邊的網格單元中。
因此,為了解決這個問題,我們對輸出執行 sigmoid 函數,將輸出壓縮到區間 0 到 1 之間,有效確保中心處于執行預測的網格單元中。
邊界框的維度
我們對輸出執行對數空間變換,然后乘錨點,來預測邊界框的維度。
檢測器輸出在最終預測之前的變換過程,圖源:http://christopher5106.github.io/
得出的預測 bw 和 bh 使用圖像的高和寬進行歸一化。即,如果包含目標(狗)的框的預測 bx 和 by 是 (0.3, 0.8),那么 13 x 13 特征圖的實際寬和高是 (13 x 0.3, 13 x 0.8)。
Objectness 分數
Object 分數表示目標在邊界框內的概率。紅色網格和相鄰網格的 Object 分數應該接近 1,而角落處的網格的 Object 分數可能接近 0。
objectness 分數的計算也使用 sigmoid 函數,因此它可以被理解為概率。
類別置信度
類別置信度表示檢測到的對象屬于某個類別的概率(如狗、貓、香蕉、汽車等)。在 v3 之前,YOLO 需要對類別分數執行 softmax 函數操作。
但是,YOLO v3 舍棄了這種設計,作者選擇使用 sigmoid 函數。因為對類別分數執行 softmax 操作的前提是類別是互斥的。簡言之,如果對象屬于一個類別,那么必須確保其不屬于另一個類別。這在我們設置檢測器的 COCO 數據集上是正確的。但是,當出現類別「女性」(Women)和「人」(Person)時,該假設不可行。這就是作者選擇不使用 Softmax 激活函數的原因。
在不同尺度上的預測
YOLO v3 在 3 個不同尺度上進行預測。檢測層用于在三個不同大小的特征圖上執行預測,特征圖步幅分別是 32、16、8。這意味著,當輸入圖像大小是 416 x 416 時,我們在尺度 13 x 13、26 x 26 和 52 x 52 上執行檢測。
該網絡在第一個檢測層之前對輸入圖像執行下采樣,檢測層使用步幅為 32 的層的特征圖執行檢測。隨后在執行因子為 2 的上采樣后,并與前一個層的特征圖(特征圖大小相同)拼接。另一個檢測在步幅為 16 的層中執行。重復同樣的上采樣步驟,最后一個檢測在步幅為 8 的層中執行。
在每個尺度上,每個單元使用 3 個錨點預測 3 個邊界框,錨點的總數為 9(不同尺度的錨點不同)。
作者稱這幫助 YOLO v3 在檢測較小目標時取得更好的性能,而這正是 YOLO 之前版本經常被抱怨的地方。上采樣可以幫助該網絡學習細粒度特征,幫助檢測較小目標。
輸出處理
對于大小為 416 x 416 的圖像,YOLO 預測 ((52 x 52) + (26 x 26) + 13 x 13)) x 3 = 10647 個邊界框。但是,我們的示例中只有一個對象——一只狗。那么我們怎么才能將檢測次數從 10647 減少到 1 呢?
目標置信度閾值:首先,我們根據它們的 objectness 分數過濾邊界框。通常,分數低于閾值的邊界框會被忽略。
非極大值抑制:非極大值抑制(NMS)可解決對同一個圖像的多次檢測的問題。例如,紅色網格單元的 3 個邊界框可以檢測一個框,或者臨近網格可檢測相同對象。
實現
YOLO 只能檢測出屬于訓練所用數據集中類別的對象。我們的檢測器將使用官方權重文件,這些權重通過在 COCO 數據集上訓練網絡而獲得,因此我們可以檢測 80 個對象類別。
該教程的第一部分到此結束。這部分詳細講解了 YOLO 算法。如果你想深度了解 YOLO 的工作原理、訓練過程和與其他檢測器的性能規避,可閱讀原始論文:
1. YOLO V1: You Only Look Once: Unified, Real-Time Object Detection (https://arxiv.org/pdf/1506.02640.pdf)
2. YOLO V2: YOLO9000: Better, Faster, Stronger (https://arxiv.org/pdf/1612.08242.pdf)
3. YOLO V3: An Incremental Improvement (https://pjreddie.com/media/files/papers/YOLOv3.pdf)
4. Convolutional Neural Networks (http://cs231n.github.io/convolutional-networks/)
5. Bounding Box Regression (Appendix C) (https://arxiv.org/pdf/1311.2524.pdf)
6. IoU?
7. Non maximum suppresion?
8. PyTorch Official Tutorial (http://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html)
第二部分:創建 YOLO 網絡層級
以下是從頭實現 YOLO v3 檢測器的第二部分教程,我們將基于前面所述的基本概念使用 PyTorch 實現 YOLO 的層級,即創建整個模型的基本構建塊。
這一部分要求讀者已經基本了解 YOLO 的運行方式和原理,以及關于 PyTorch 的基本知識,例如如何通過 nn.Module、nn.Sequential 和 torch.nn.parameter 等類來構建自定義的神經網絡架構。
開始旅程
首先創建一個存放檢測器代碼的文件夾,然后再創建 Python 文件 darknet.py。Darknet 是構建 YOLO 底層架構的環境,這個文件將包含實現 YOLO 網絡的所有代碼。同樣我們還需要補充一個名為 util.py 的文件,它會包含多種需要調用的函數。在將所有這些文件保存在檢測器文件夾下后,我們就能使用 git 追蹤它們的改變。
配置文件
官方代碼(authored in C)使用一個配置文件來構建網絡,即 cfg 文件一塊塊地描述了網絡架構。如果你使用過 caffe 后端,那么它就相當于描述網絡的.protxt 文件。
我們將使用官方的 cfg 文件構建網絡,它是由 YOLO 的作者發布的。我們可以在以下地址下載,并將其放在檢測器目錄下的 cfg 文件夾下。
配置文件下載:https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg
當然,如果你使用 Linux,那么就可以先 cd 到檢測器網絡的目錄,然后運行以下命令行。
mkdir cfg
cd cfg
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
如果你打開配置文件,你將看到如下一些網絡架構:
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
我們看到上面有四塊配置,其中 3 個描述了卷積層,最后描述了 ResNet 中常用的捷徑層或跳過連接。下面是 YOLO 中使用的 5 種層級:
1. 卷積層
[convolutional]
batch_normalize=1?
filters=64?
size=3?
stride=1?
pad=1?
activation=leaky
2. 跳過連接
[shortcut]
from=-3?
activation=linear?
跳過連接與殘差網絡中使用的結構相似,參數 from 為-3 表示捷徑層的輸出會通過將之前層的和之前第三個層的輸出的特征圖與模塊的輸入相加而得出。
3.上采樣
[upsample]
stride=2
通過參數 stride 在前面層級中雙線性上采樣特征圖。
4.路由層(Route)
[route]
layers = -4
[route]
layers = -1, 61
路由層需要一些解釋,它的參數 layers 有一個或兩個值。當只有一個值時,它輸出這一層通過該值索引的特征圖。在我們的實驗中設置為了-4,所以層級將輸出路由層之前第四個層的特征圖。
當層級有兩個值時,它將返回由這兩個值索引的拼接特征圖。在我們的實驗中為-1 和 61,因此該層級將輸出從前一層級(-1)到第 61 層的特征圖,并將它們按深度拼接。
5.YOLO
[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=80
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
YOLO 層級對應于上文所描述的檢測層級。參數 anchors 定義了 9 組錨點,但是它們只是由 mask 標簽使用的屬性所索引的錨點。這里,mask 的值為 0、1、2 表示了第一個、第二個和第三個使用的錨點。而掩碼表示檢測層中的每一個單元預測三個框??偠灾?,我們檢測層的規模為 3,并裝配總共 9 個錨點。
Net
[net]
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=16
width= 320
height = 320
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
配置文件中存在另一種塊 net,不過我不認為它是層,因為它只描述網絡輸入和訓練參數的相關信息,并未用于 YOLO 的前向傳播。但是,它為我們提供了網絡輸入大小等信息,可用于調整前向傳播中的錨點。
解析配置文件
在開始之前,我們先在 darknet.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
我們定義一個函數 parse_cfg,該函數使用配置文件的路徑作為輸入。
def parse_cfg(cfgfile):
?"""
?Takes a configuration file
?Returns a list of blocks. Each blocks describes a block in the neural
?network to be built. Block is represented as a dictionary in the list
?"""
這里的思路是解析 cfg,將每個塊存儲為詞典。這些塊的屬性和值都以鍵值對的形式存儲在詞典中。解析過程中,我們將這些詞典(由代碼中的變量 block 表示)添加到列表 blocks 中。我們的函數將返回該 block。
我們首先將配置文件內容保存在字符串列表中。下面的代碼對該列表執行預處理:
file = open(cfgfile, "r")
lines = file.read().split(" ") # store the lines in a list
lines = [x for x in lines if len(x) > 0] # get read of the empty lines?
lines = [x for x in lines if x[0] != "#"] # get rid of comments
lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces
然后,我們遍歷預處理后的列表,得到塊。
block = {}
blocks = []
for line in lines:
?if line[0] == "[": # This marks the start of a new block
?if len(block) != 0: # If block is not empty, implies it is storing values of previous block.
?blocks.append(block) # add it the blocks list
?block = {} # re-init the block
?block["type"] = line[1:-1].rstrip()?
?else:
?key,value = line.split("=")?
?block[key.rstrip()] = value.lstrip()
blocks.append(block)
return blocks
創建構建塊
現在我們將使用上面 parse_cfg 返回的列表來構建 PyTorch 模塊,作為配置文件中的構建塊。
列表中有 5 種類型的層。PyTorch 為 convolutional 和 upsample 提供預置層。我們將通過擴展 nn.Module 類為其余層寫自己的模塊。
create_modules 函數使用 parse_cfg 函數返回的 blocks 列表:
def create_modules(blocks):
?net_info = blocks[0] #Captures the information about the input and pre-processing?
?module_list = nn.ModuleList()
?prev_filters = 3
?output_filters = []
在迭代該列表之前,我們先定義變量 net_info,來存儲該網絡的信息。
nn.ModuleList
我們的函數將會返回一個 nn.ModuleList。這個類幾乎等同于一個包含 nn.Module 對象的普通列表。然而,當添加 nn.ModuleList 作為 nn.Module 對象的一個成員時(即當我們添加模塊到我們的網絡時),所有 nn.ModuleList 內部的 nn.Module 對象(模塊)的 parameter 也被添加作為 nn.Module 對象(即我們的網絡,添加 nn.ModuleList 作為其成員)的 parameter。
當我們定義一個新的卷積層時,我們必須定義它的卷積核維度。雖然卷積核的高度和寬度由 cfg 文件提供,但卷積核的深度是由上一層的卷積核數量(或特征圖深度)決定的。這意味著我們需要持續追蹤被應用卷積層的卷積核數量。我們使用變量 prev_filter 來做這件事。我們將其初始化為 3,因為圖像有對應 RGB 通道的 3 個通道。
路由層(route layer)從前面層得到特征圖(可能是拼接的)。如果在路由層之后有一個卷積層,那么卷積核將被應用到前面層的特征圖上,較精確來說是路由層得到的特征圖。因此,我們不僅需要追蹤前一層的卷積核數量,還需要追蹤之前每個層。隨著不斷地迭代,我們將每個模塊的輸出卷積核數量添加到 output_filters 列表上。
現在,我們的思路是迭代模塊的列表,并為每個模塊創建一個 PyTorch 模塊。
?for index, x in enumerate(blocks[1:]):
?module = nn.Sequential()
?#check the type of block
?#create a new module for the block
?#append to module_list
nn.Sequential 類被用于按順序地執行 nn.Module 對象的一個數字。如果你查看 cfg 文件,你會發現,一個模塊可能包含多于一個層。例如,一個 convolutional 類型的模塊有一個批量歸一化層、一個 leaky ReLU 激活層以及一個卷積層。我們使用 nn.Sequential 將這些層串聯起來,得到 add_module 函數。例如,以下展示了我們如何創建卷積層和上采樣層的例子:
?if (x["type"] == "convolutional"):
?#Get the info about the layer
?activation = x["activation"]
?try:
?batch_normalize = int(x["batch_normalize"])
?bias = False
?except:
?batch_normalize = 0
?bias = True
?filters= int(x["filters"])
?padding = int(x["pad"])
?kernel_size = int(x["size"])
?stride = int(x["stride"])
?if padding:
?pad = (kernel_size - 1) // 2
?else:
?pad = 0
?#Add the convolutional layer
?conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
?module.add_module("conv_{0}".format(index), conv)
?#Add the Batch Norm Layer
?if batch_normalize:
?bn = nn.BatchNorm2d(filters)
?module.add_module("batch_norm_{0}".format(index), bn)
?#Check the activation.?
?#It is either Linear or a Leaky ReLU for YOLO
?if activation == "leaky":
?activn = nn.LeakyReLU(0.1, inplace = True)
?module.add_module("leaky_{0}".format(index), activn)
?#If it"s an upsampling layer
?#We use Bilinear2dUpsampling
?elif (x["type"] == "upsample"):
?stride = int(x["stride"])
?upsample = nn.Upsample(scale_factor = 2, mode = "bilinear")
?module.add_module("upsample_{}".format(index), upsample)
路由層/捷徑層
接下來,我們來寫創建路由層(Route Layer)和捷徑層(Shortcut Layer)的代碼:
?#If it is a route layer
?elif (x["type"] == "route"):
?x["layers"] = x["layers"].split(",")
?#Start of a route
?start = int(x["layers"][0])
?#end, if there exists one.
?try:
?end = int(x["layers"][1])
?except:
?end = 0
?#Positive anotation
?if start > 0:?
?start = start - index
?if end > 0:
?end = end - index
?route = EmptyLayer()
?module.add_module("route_{0}".format(index), route)
?if end < 0:
?filters = output_filters[index + start] + output_filters[index + end]
?else:
?filters= output_filters[index + start]
?#shortcut corresponds to skip connection
?elif x["type"] == "shortcut":
?shortcut = EmptyLayer()
?module.add_module("shortcut_{}".format(index), shortcut)
創建路由層的代碼需要做一些解釋。首先,我們提取關于層屬性的值,將其表示為一個整數,并保存在一個列表中。
然后我們得到一個新的稱為 EmptyLayer 的層,顧名思義,就是空的層。
route = EmptyLayer()
其定義如下:
class EmptyLayer(nn.Module):
?def __init__(self):
?super(EmptyLayer, self).__init__()
等等,一個空的層?
現在,一個空的層可能會令人困惑,因為它沒有做任何事情。而 Route Layer 正如其它層將執行某種操作(獲取之前層的拼接)。在 PyTorch 中,當我們定義了一個新的層,我們在子類 nn.Module 中寫入層在 nn.Module 對象的 forward 函數的運算。
對于在 Route 模塊中設計一個層,我們必須建立一個 nn.Module 對象,其作為 layers 的成員被初始化。然后,我們可以寫下代碼,將 forward 函數中的特征圖拼接起來并向前饋送。最后,我們執行網絡的某個 forward 函數的這個層。
但拼接操作的代碼相當地短和簡單(在特征圖上調用 torch.cat),像上述過程那樣設計一個層將導致不必要的抽象,增加樣板代碼。取而代之,我們可以將一個假的層置于之前提出的路由層的位置上,然后直接在代表 darknet 的 nn.Module 對象的 forward 函數中執行拼接運算。(如果感到困惑,我建議你讀一下 nn.Module 類在 PyTorch 中的使用)。
在路由層之后的卷積層會把它的卷積核應用到之前層的特征圖(可能是拼接的)上。以下的代碼更新了 filters 變量以保存路由層輸出的卷積核數量。
if end < 0:
?#If we are concatenating maps
?filters = output_filters[index + start] + output_filters[index + end]
else:
?filters= output_filters[index + start]
捷徑層也使用空的層,因為它還要執行一個非常簡單的操作(加)。沒必要更新 filters 變量,因為它只是將前一層的特征圖添加到后面的層上而已。
YOLO 層
最后,我們將編寫創建 YOLO 層的代碼:
?#Yolo is the detection layer
?elif x["type"] == "yolo":
?mask = x["mask"].split(",")
?mask = [int(x) for x in mask]
?anchors = x["anchors"].split(",")
?anchors = [int(a) for a in anchors]
?anchors = [(anchors[i], anchors[i+1]) for i in range(0, len(anchors),2)]
?anchors = [anchors[i] for i in mask]
?detection = DetectionLayer(anchors)
?module.add_module("Detection_{}".format(index), detection)
我們定義一個新的層 DetectionLayer 保存用于檢測邊界框的錨點。
檢測層的定義如下:
class DetectionLayer(nn.Module):
?def __init__(self, anchors):
?super(DetectionLayer, self).__init__()
?self.anchors = anchors
在這個回路結束時,我們做了一些統計(bookkeeping.)。
?module_list.append(module)
?prev_filters = filters
?output_filters.append(filters)
這總結了此回路的主體。在 create_modules 函數后,我們獲得了包含 net_info 和 module_list 的元組。
return (net_info, module_list)
測試代碼
你可以在 darknet.py 后通過輸入以下命令行測試代碼,運行文件。
blocks = parse_cfg("cfg/yolov3.cfg")
print(create_modules(blocks))
你會看到一個長列表(確切來說包含 106 條),其中元素看起來如下所示:
?(9): Sequential(
?(conv_9): Conv2d (128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
?(batch_norm_9): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
?(leaky_9): LeakyReLU(0.1, inplace)
?)
?(10): Sequential(
?(conv_10): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
?(batch_norm_10): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
?(leaky_10): LeakyReLU(0.1, inplace)
?)
?(11): Sequential(
?(shortcut_11): EmptyLayer(
?)
?)
第三部分:實現網絡的前向傳播
第二部分中,我們實現了 YOLO 架構中使用的層。這部分,我們計劃用 PyTorch 實現 YOLO 網絡架構,這樣我們就能生成給定圖像的輸出了。
我們的目標是設計網絡的前向傳播。
先決條件
閱讀本教程前兩部分;
PyTorch 基礎知識,包括如何使用 nn.Module、nn.Sequential 和 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。我們用 members、blocks、net_info 和 module_list 對網絡進行初始化。
實現該網絡的前向傳播
該網絡的前向傳播通過覆寫 nn.Module 類別的 forward 方法而實現。
forward 主要有兩個目的。一,計算輸出;二,盡早處理的方式轉換輸出檢測特征圖(例如轉換之后,這些不同尺度的檢測圖就能夠串聯,不然會因為不同維度不可能實現串聯)。
def forward(self, x, CUDA):
?modules = self.blocks[1:]
?outputs = {} #We cache the outputs for the route layer
forward 函數有三個參數:self、輸入 x 和 CUDA(如果是 true,則使用 GPU 來加速前向傳播)。
這里,我們迭代 self.block[1:] 而不是 self.blocks,因為 self.blocks 的第一個元素是一個 net 塊,它不屬于前向傳播。
由于路由層和捷徑層需要之前層的輸出特征圖,我們在字典 outputs 中緩存每個層的輸出特征圖。關鍵在于層的索引,且值對應特征圖。
正如 create_module 函數中的案例,我們現在迭代 module_list,它包含了網絡的模塊。需要注意的是這些模塊是以在配置文件中相同的順序添加的。這意味著,我們可以簡單地讓輸入通過每個模塊來得到輸出。
write = 0 #This is explained a bit later
for i, module in enumerate(modules):?
?module_type = (module["type"])
卷積層和上采樣層
如果該模塊是一個卷積層或上采樣層,那么前向傳播應該按如下方式工作:
?if module_type == "convolutional" or module_type == "upsample":
?x = self.module_list[i](x)
路由層/捷徑層
如果你查看路由層的代碼,我們必須說明兩個案例(正如第二部分中所描述的)。對于第一個案例,我們必須使用 torch.cat 函數將兩個特征圖級聯起來,第二個參數設為 1。這是因為我們希望將特征圖沿深度級聯起來。(在 PyTorch 中,卷積層的輸入和輸出的格式為`B X C X H X 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(檢測層)
YOLO 的輸出是一個卷積特征圖,包含沿特征圖深度的邊界框屬性。邊界框屬性由彼此堆疊的單元格預測得出。因此,如果你需要在 (5,6) 處訪問單元格的第二個邊框,那么你需要通過 map[5,6, (5+C): 2*(5+C)] 將其編入索引。這種格式對于輸出處理過程(例如通過目標置信度進行閾值處理、添加對中心的網格偏移、應用錨點等)很不方便。
另一個問題是由于檢測是在三個尺度上進行的,預測圖的維度將是不同的。雖然三個特征圖的維度不同,但對它們執行的輸出處理過程是相似的。如果能在單個張量而不是三個多帶帶張量上執行這些運算,就太好了。
為了解決這些問題,我們引入了函數 predict_transform。
變換輸出
函數 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 flag(可選)。
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)
錨點的維度與 net 塊的 height 和 width 屬性一致。這些屬性描述了輸入圖像的維度,比檢測圖的規模大(二者之商即是步幅)。因此,我們必須使用檢測特征圖的步幅分割錨點。
?anchors = [(a[0]/stride, a[1]/stride) for a in anchors]
現在,我們需要根據第一部分討論的公式變換輸出。
對 (x,y) 坐標和 objectness 分數執行 Sigmoid 函數操作。
?#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]))
最后,我們要將檢測圖的大小調整到與輸入圖像大小一致。邊界框屬性根據特征圖的大小而定(如 13 x 13)。如果輸入圖像大小是 416 x 416,那么我們將屬性乘 32,或乘 stride 變量。
prediction[:,:,:4] *= stride
loop 部分到這里就大致結束了。
函數結束時會返回預測結果:
?return prediction
重新訪問的檢測層
我們已經變換了輸出張量,現在可以將三個不同尺度的檢測圖級聯成一個大的張量。注意這必須在變換之后進行,因為你無法級聯不同空間維度的特征圖。變換之后,我們的輸出張量把邊界框表格呈現為行,級聯就比較可行了。
一個阻礙是我們無法初始化空的張量,再向其級聯一個(不同形態的)非空張量。因此,我們推遲收集器(容納檢測的張量)的初始化,直到獲得第一個檢測圖,再把這些檢測圖級聯起來。
注意 write = 0 在函數 forward 的 loop 之前。write flag 表示我們是否遇到第一個檢測。如果 write 是 0,則收集器尚未初始化。如果 write 是 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
測試前向傳播
下面的函數將創建一個偽造的輸入,我們可以將該輸入傳入我們的網絡。在寫該函數之前,我們可以使用以下命令行將這張圖像保存到工作目錄:
wget https://github.com/ayooshkathuria/pytorch-yolo-v3/raw/master/dog-cycle-car.png
也可以直接下載圖像: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)) #Resize to the input dimension
?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() #Convert to float
?img_ = Variable(img_) # Convert to Variable
?return img_
我們需要鍵入以下代碼:
model = Darknet("cfg/yolov3.cfg")
inp = get_test_input()
pred = model(inp)
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×10647×85,第一個維度為批量大小,這里我們只使用了單張圖像。對于批量中的圖像,我們會有一個 100647×85 的表,它的每一行表示一個邊界框(4 個邊界框屬性、1 個 objectness 分數和 80 個類別分數)。
現在,我們的網絡有隨機權重,并且不會輸出正確的類別。我們需要為網絡加載權重文件,因此可以利用官方權重文件。
下載預訓練權重
下載權重文件并放入檢測器目錄下,我們可以直接使用命令行下載:
wget https://pjreddie.com/media/files/yolov3.weights
也可以通過該地址下載:https://pjreddie.com/media/files/yolov3.weights
理解權重文件
官方的權重文件是一個二進制文件,它以序列方式儲存神經網絡權重。
我們必須小心地讀取權重,因為權重只是以浮點形式儲存,沒有其它信息能告訴我們到底它們屬于哪一層。所以如果讀取錯誤,那么很可能權重加載就全錯了,模型也完全不能用。因此,只閱讀浮點數,無法區別權重屬于哪一層。因此,我們必須了解權重是如何存儲的。
首先,權重只屬于兩種類型的層,即批歸一化層(batch norm layer)和卷積層。這些層的權重儲存順序和配置文件中定義層級的順序完全相同。所以,如果一個 convolutional 后面跟隨著 shortcut 塊,而 shortcut 連接了另一個 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.
在循環過程中,我們首先檢查 convolutional 模塊是否有 batch_normalize(True)。基于此,我們加載權重。
?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 檢查結果是 True,則我們按以下方式加載權重:
?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_normalize 的檢查結果不是 True,只需要加載卷積層的偏置項。
?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")
通過模型構建和權重加載,我們終于可以開始進行目標檢測了。未來,我們還將介紹如何利用 objectness 置信度閾值和非極大值抑制生成最終的檢測結果。
原文鏈接:https://medium.com/paperspace/tutorial-on-implementing-yolo-v3-from-scratch-in-pytorch-part-1-a0054d38ec78
歡迎加入本站公開興趣群?
商業智能與數據分析群
興趣范圍包括各種讓數據產生價值的辦法,實際應用案例分享與討論,分析工具,ETL工具,數據倉庫,數據挖掘工具,報表系統等全方位知識
QQ群:81035754
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/4741.html
摘要:來自原作者,快如閃電,可稱目標檢測之光。實現教程去年月就出現了,實現一直零零星星。這份實現,支持用自己的數據訓練模型?,F在可以跑腳本了來自原作者拿自己的數據集訓練快速訓練這個就是給大家一個粗略的感受,感受的訓練過程到底是怎樣的。 來自YOLOv3原作者YOLOv3,快如閃電,可稱目標檢測之光。PyTorch實現教程去年4月就出現了,TensorFlow實現一直零零星星?,F在,有位熱心公益的程...
摘要:的安裝下載好之后雙擊打開可執行安裝文件選擇安裝目錄,需要的內存較多,建議將其安裝在盤或者盤,不建議放在系統盤盤。 yolov5無從下手?一篇就夠的保姆級教程,202...
摘要:值得一提的是,基于百度自研的開源深度學習平臺的實現,參考了論文,增加了,等處理,精度相比于原作者的實現提高了個絕對百分點,在此基礎上加入最終精度相比原作者提高個絕對百分點。 YOLO作為目標檢測領域的創新技術,一經推出就受到開發者的廣泛關注。值得一提的是,基于百度自研的開源深度學習平臺PaddlePaddle的YOLO v3實現,參考了論文【Bag of Tricks for Imag...
閱讀 3511·2023-04-25 14:57
閱讀 2560·2021-11-22 14:56
閱讀 2079·2021-09-29 09:45
閱讀 1761·2021-09-22 15:53
閱讀 3313·2021-08-25 09:41
閱讀 896·2019-08-29 15:22
閱讀 3289·2019-08-29 13:22
閱讀 3122·2019-08-29 13:08