您當前的位置:首頁 > 書法

【目標檢測程式碼實戰】從零開始動手實現yolov3:訓練篇(一)

作者:由 嘰歪小糖豆 發表于 書法時間:2019-11-26

​前言

在前面幾篇文章中小糖豆為大家講解了yolo系列演算法的演變。俗話說,光說不練假把式。接下來小糖豆將帶領大家從零開始,親自動手實現yolov3的訓練與預測。

本教程說明:

需要讀者已經基本瞭解pytorch的基礎用法以及yolov3的演算法原理(不太瞭解的小夥伴可以移步yolo系列演算法三);

參考的程式碼:

https://

github。com/eriklinderno

ren/PyTorch-YOLOv3

小糖豆在原始碼基礎上修改了部分原始碼(接下來會按照修改後的原始碼進行講解,主要修改點包括tensorboard視覺化部分、voc資料集標籤製作部分、模型引數配置等),同時增加了camera。py(呼叫usb相機實現yolov3實時檢測,呼叫方式為:python3 camera。py),有需要的小夥伴可以關注公眾號並後臺回覆:yolov3,免費獲取原始碼。

配置

在開始講解前,小糖豆建議大家先將原始碼下載下來配置好,先執行一下看看模型效果。在跟隨小糖豆講解的過程中每一步除錯一下,多嘗試多動手,可以起到事半功倍的奇效。

我們先大致看下原始碼的組成部分:

【目標檢測程式碼實戰】從零開始動手實現yolov3:訓練篇(一)

checkpoints:模型訓練節點存放地址

config:模型和資料集的配置檔案

data:訓練所用的資料集

output:影象測試輸出地址

utils:模型構建所需的模組(資料增強,資料解析,tensorboard視覺化等)

weights:預訓練模型檔案

camera。py:模式實時預測

detect。py:模型預測

models。py:模型構建模組

test。py:模型驗證

train。py:模型訓練

我們從train。py檔案開始分析:

初始化

匯入模組:

from __future__ import division

from models import *

from utils。utils import *

from utils。datasets import *

from utils。parse_config import *

from test import evaluate

from terminaltables import AsciiTable

import os

import sys

import time

import datetime

import argparse

import cv2

import torch

import torchvision

from torch。utils。data import DataLoader

from torchvision import datasets

from torchvision import transforms

from torch。autograd import Variable

import torch。optim as optim

from tensorboardX import SummaryWriter

命令列引數解析:

if __name__ == “__main__”:

parser = argparse。ArgumentParser()

parser。add_argument(“——epochs”, type=int, default=100, help=“number of epochs”)

parser。add_argument(“——learning_rate”, default=0。001, help=“learning rate”)

parser。add_argument(“——batch_size”, type=int, default=12, help=“size of each image batch”)

parser。add_argument(“——gradient_accumulations”, type=int, default=2, help=“number of gradient accums before step”)

parser。add_argument(“——model_def”, type=str, default=“config/yolov3-voc0712。cfg”, help=“path to model definition file”)

parser。add_argument(“——data_config”, type=str, default=“config/voc0712。data”, help=“path to data config file”)

parser。add_argument(“——pretrained_weights”, type=str, default=‘。/weights/darknet53。conv。74’,help=“if specified starts from checkpoint model”)

parser。add_argument(“——n_cpu”, type=int, default=1, help=“number of cpu threads to use during batch generation”)

parser。add_argument(“——img_size”, type=int, default=416, help=“size of each image dimension”)

parser。add_argument(“——checkpoint_interval”, type=int, default=1, help=“interval between saving model weights”)

parser。add_argument(“——evaluation_interval”, type=int, default=1, help=“interval evaluations on validation set”)

parser。add_argument(“——compute_map”, default=True, help=“if True computes mAP every tenth batch”)

parser。add_argument(“——multiscale_training”, default=True, help=“allow for multi-scale training”)

parser。add_argument(“——conf_thres”, type=float, default=0。8, help=“object confidence threshold”)

parser。add_argument(“——nms_thres”, type=float, default=0。4, help=“iou thresshold for non-maximum suppression”)

opt = parser。parse_args()

print(“type(opt) ——> ”,type(opt))

print(opt)

argparse是一個命令列引數解析模組,add_argument新增可能會發生變化的引數(比如模型的配置檔案,訓練批大小等),parse_args()函式將新增的引數進行解析。print列印結果如下:

type(opt) ——>

Namespace(batch_size=12, checkpoint_interval=1, compute_map=True, conf_thres=0。8, data_config=‘config/voc0712。data’, epochs=100, evaluation_interval=1, gradient_accumulations=2, img_size=416, learning_rate=0。001, model_def=‘config/yolov3-voc0712。cfg’, multiscale_training=True, n_cpu=1, nms_thres=0。4, pretrained_weights=‘。/weights/darknet53。conv。74’)

模型構建:

device = torch。device(“cuda” if torch。cuda。is_available() else “cpu”)

model = Darknet(opt。model_def,img_size=opt。img_size)。to(device)

使用Darknet類定義一個model 物件,Darknet類的定義位於model。py。在定義物件的時候會執行類的建構函式__init__。

class Darknet(nn。Module):

def __init__(self, config_path, img_size=416): #config_path = “config/yolov3-voc0712。cfg”

super(Darknet, self)。__init__()

self。module_defs = parse_model_config(config_path) #解析模型引數

self。hyperparams, self。module_list = create_modules(self。module_defs)

self。yolo_layers = [layer[0] for layer in self。module_list if hasattr(layer[0], “metrics”)]

self。img_size = img_size

self。seen = 0

self。header_info = np。array([0, 0, 0, self。seen, 0], dtype=np。int32)

Darknet類繼承自pytorch的nn。Module,parse_model_config函式以模型的配置檔案為引數,返回的列表作為create_modules函式引數來構建pytorch模組。

我們先看看parse_model_config函式:

def parse_model_config(path):

“”“Parses the yolo-v3 layer configuration file and returns module definitions”“”

file = open(path, ‘r’)

lines = file。read()。split(‘\n’)

lines = [x for x in lines if x and not x。startswith(‘#’)]

lines = [x。rstrip()。lstrip() for x in lines] # get rid of fringe whitespaces

首先讀取cfg檔案,將內容儲存在lines列表中,儲存的時候去掉註釋行、空行和無用空格。

module_defs = []

for line in lines:

if line。startswith(‘[’): # This marks the start of a new block

module_defs。append({})

module_defs[-1][‘type’] = line[1:-1]。rstrip()

if module_defs[-1][‘type’] == ‘convolutional’:

module_defs[-1][‘batch_normalize’] = 0

else:

key, value = line。split(“=”)

value = value。strip()

module_defs[-1][key。rstrip()] = value。strip()

return module_defs

遍歷lines,將cfg檔案中的每個塊儲存為dict。這些塊的屬性和值都以鍵值的形式儲存在dict中。

create_modules函式使用返回的module_defs列表構建pytorch模組。

def create_modules(module_defs):

“”“

Constructs module list of layer blocks from module configuration in module_defs

”“”

hyperparams = module_defs。pop(0)

在迭代module_defs列表前,先獲取模型的超引數。

output_filters = [int(hyperparams[“channels”])]

module_list = nn。ModuleList()

create_modules函式會返回超引數和module_list。module_list是一個nn。Sequential(),這個類等同於一個包含

nn.Module

物件的普通列表。

定義一個卷積層必須定義它的卷積核維度。雖然卷積核的高度和寬度由 cfg 檔案提供,但卷積核的深度是由上一層的卷積核數量(或特徵圖深度)決定的。這意味著我們需要持續追蹤被應用卷積層的卷積核數量。

路由層(route layer)從前面層得到特徵圖(可能是拼接的)。如果在路由層之後有一個卷積層,那麼卷積核將被應用到前面層的特徵圖上,精確來說是路由層得到的特徵圖。因此,我們不僅需要追蹤前一層的卷積核數量,還需要追蹤之前每個層。

為了實現上述過程,在迭代過程中我們將每個模組的輸出卷積核數量新增到 output_filters列表中。接下來我們可以開始迭代module_defs列表了:

for module_i, module_def in enumerate(module_defs):

modules = nn。Sequential()

nn。Sequential類被用於按順序地執行

nn.Module

物件中的模型層。在 cfg 檔案中一個模組可能包含多個層。例如,一個 convolutional 型別的模組有一個批次歸一化層、一個 leaky ReLU 啟用層以及一個卷積層。我們使用nn。Sequential 將這些層串聯起來,使用 add_module 函式建立單個模型塊。以下展示了建立卷積層和上取樣層的例子:

if module_def[“type”] == “convolutional”:

bn = int(module_def[“batch_normalize”])

filters = int(module_def[“filters”])

kernel_size = int(module_def[“size”])

pad = (kernel_size - 1) // 2

modules。add_module(

f“conv_{module_i}”,

nn。Conv2d(

in_channels=output_filters[-1],

out_channels=filters,

kernel_size=kernel_size,

stride=int(module_def[“stride”]),

padding=pad,

bias=not bn,

),

if bn:

modules。add_module(f“batch_norm_{module_i}”, nn。BatchNorm2d(filters, momentum=0。9, eps=1e-5))

if module_def[“activation”] == “leaky”:

modules。add_module(f“leaky_{module_i}”, nn。LeakyReLU(0。1))

elif module_def[“type”] == “upsample”:

upsample = Upsample(scale_factor=int(module_def[“stride”]), mode=“nearest”)

modules。add_module(f“upsample_{module_i}”, upsample)

接下來我們看看建立路由層(route layer)和短接層(shortcut layer):

elif module_def[“type”] == “route”:

layers = [int(x) for x in module_def[“layers”]。split(“,”)]

filters = sum([output_filters[1:][i] for i in layers])

modules。add_module(f“route_{module_i}”, EmptyLayer())

elif module_def[“type”] == “shortcut”:

filters = output_filters[1:][int(module_def[“from”])]

modules。add_module(f“shortcut_{module_i}”, EmptyLayer())

建立路由層時,首先提取出關於層屬性的值,儲存在layers列表中。每一次訪問到路由層時,它的layers引數可能有不止一個值。

當只有一個值時,它輸出這一層透過該值索引的特徵圖。比如說-4意味著輸出路由層往前第四個特徵圖。

當有兩個值時,它將返回這兩個值索引的拼接後的特徵圖。比如-1和61意味著輸出前一層(-1)和第61層的特徵圖,並將它們按深度拼接。

由於路由層僅僅涉及到特徵圖的拼接,沒有多餘的操作,因此在建立模組時使用一個不執行任何操作的模型層,在執行前向傳播時完成拼接即可。

同樣地,建立短接層時僅僅涉及到特徵圖的相加,也採用空白模型層的方式。

所謂的空白模型層如下,什麼操作也沒有:

class EmptyLayer(nn。Module):

“”“Placeholder for ‘route’ and ‘shortcut’ layers”“”

def __init__(self):

super(EmptyLayer, self)。__init__()

最後一個模組是yolo層,這一層儲存了模型中的一些重要資訊,如anchor box尺寸,類別數量,影象尺寸。在這裡我們定義了一個YOLOLayer模型層,該模型層的具體細節等到前向傳播的時候在介紹,這裡小夥伴們只需要預設它是一個模型層就可以了。

elif module_def[“type”] == “yolo”:

anchor_idxs = [int(x) for x in module_def[“mask”]。split(“,”)]

# Extract anchors

anchors = [int(x) for x in module_def[“anchors”]。split(“,”)]

anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]

anchors = [anchors[i] for i in anchor_idxs]

num_classes = int(module_def[“classes”])

img_size = int(hyperparams[“height”])

# Define detection layer

yolo_layer = YOLOLayer(anchors, num_classes, img_size)

modules。add_module(f“yolo_{module_i}”, yolo_layer)

# Register module list and number of output filters

module_list。append(modules)

output_filters。append(filters)

return hyperparams, module_list

想觀察下模型架構的小夥伴可以執行下models。py。小糖豆在檔案最後加了幾行程式碼,可以列印構建的模型塊。

if __name__ == “__main__”:

module_list = create_modules(parse_model_config(“。/config/yolov3-voc0712。cfg”))

print(module_list)

會打印出模型資訊,包含106個模型塊,如下:

【目標檢測程式碼實戰】從零開始動手實現yolov3:訓練篇(一)

model。apply(weights_init_normal)

構建好模型後,對模型的引數初始化。

def weights_init_normal(m):

classname = m。__class__。__name__

if classname。find(“Conv”) != -1:

torch。nn。init。normal_(m。weight。data, 0。0, 0。02)

elif classname。find(“BatchNorm2d”) != -1:

torch。nn。init。normal_(m。weight。data, 1。0, 0。02)

torch。nn。init。constant_(m。bias。data, 0。0)

接下來載入預訓練模型:

if opt。pretrained_weights:

if opt。pretrained_weights。endswith(“。pth”):

model。load_state_dict(torch。load(opt。pretrained_weights))

else:

model。load_darknet_weights(opt。pretrained_weights)

第一次訓練時,一般會使用model。load_darknet_weights載入darknet預訓練模型進行finetuning。

除此之外,訓練過程可能會分好幾個階段,也可能會由於空間不足而意外中止,此時可以執行model。load_state_dict載入訓練中儲存的中間節點的模型檔案。

for i,p in enumerate(model。named_parameters()):

if i == 156: #darknet部分引數不更新

break

p[1]。requires_grad = False

如果不想改變yolov3的backbone基礎網路部分的引數,可以透過requires_grad來設定。

到這一步模型已經搭建完畢,小糖豆將在下篇文章中講解資料集的載入與預處理。

未完待續……

標簽: module  def  __  add  type