Pytorch基础知识(8)多目标检测

目标检测是对图像中存在的目标进行定位和分类的过程。识别出的物体在图像中显示为边界框。一般的目标检测有两种方法:基于区域提议的和基于回归/分类的。在本章中,我们将使用一个名为YOLO的基于回归/分类的方法。YOLO-v3是该系列的其中一个版本,在精度方面比以前的(YOLOV1、YOLOV2)版本表现更好。因此,本章将重点介绍使用PyTorch开发的Yolo-v3。
在本章中,我们将学习如何实现YOLO-v3算法,并使用PyTorch训练和部署它。
我们将介绍以下内容:

  • 创建数据集
  • 构建YOLOV3模型
  • 定义损失函数
  • 训练模型
  • 模型部署

创建数据集

使用COCO数据集训练YOLOV3模型。COCO是一个大规模的目标检测、分割和看图说话数据集,包含80个类别。
在本教程中,您将学习如何创建自定义数据集、执行数据变换和定义数据加载器。

数据准备

  1. 下载以下GitHub存储库:https:/​/​github.​com/​pjreddie/darknet
  2. 从下载的存储库中获取darknet/scripts/get_coco_dataset.sh文件
  3. 创建一个名为data的文件夹,其中包含您的脚本,并将get_coco_dataset.sh复制到该文件夹中。
  4. 接下来,在data文件夹打开一个Terminal并执行get_coco_dataset.sh文件。修改get_coco_dataset.sh中的coco目录为cocoapi目录
  5. 该脚本将完整的COCO数据集下载到名为cocoapi的子文件夹中。在脚本所在的位置创建一个名为config的文件夹,并将darknet/cfg/yolov3.cfg文件复制到config文件夹中。
  6. cocoapi文件夹的内容截图如下所示:
    Pytorch基础知识(8)多目标检测_第1张图片
    在images文件夹中,应该有两个名为train2014和val2014 的子文件夹,分别包含82783张和40504张图像。
    在labels文件夹中,应该有两个名为train2014和val2014 with的子文件夹,分别包含82081和40137个文本文件。这些文本文件包含图像中对象的边界框坐标。
    例如,val2014文件夹下的COCO_val2014_000000000133.txt文本文件包含如下坐标:
    59 0.510930 0.442073 0.978141 0.872188
    77 0.858305 0.073521 0.074922 0.059833
    第一个数字是对象ID,接下来的四个数字是[xc, yc, w, h]格式的归一化后的边界框坐标,其中xc, yc是中心坐标,w, h是边界框的宽度和高度。
    另外,trainvalno5k.txt文件包含一个117264张图像的列表,这些图像将用于训练模型。这个列表是train2014和val2014子文件夹中的图像(除了5000个图像)的组合。这个5k.txt文件包含一个5000张图片的列表,这些图片将用于验证。5k.txt文件有问题,有几张图片不存在,需要删除对应的目录,不然验证时候出错。
    最后,从下面的链接中获取coco.names文件,并将其放到data文件夹中:
    https:/​/​github.​com/​pjreddie/​darknet/​blob/​master/​data/​coco.​names
    coco.names文件包含COCO数据集中的80个对象类别列表

现在我们已经下载了COCO数据集,我们将使用PyTorch的dataset和Dataloader类创建训练和验证数据集和数据加载器。

创建自己的COCO数据集

在本节中,我们将定义CocoDataset类,并展示来自训练和验证数据集的一些示例图像。

#1. 首先定义一个自己数据集类
form torch.utils.data import Dataset
from PIL import Image
import torchvision.transforms.functional as TF
import os
import numpy as np
import torch
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 定义CocoDataset类
class CocoDataset(Dataset):
	def __init__(self, path2listFile, transform=None, trans_params=None):
		with open(path2listFile, "r") as file:
			self.path2imgs = file.readlines()
			self.path2labels=[path.replace("images", "labels").replace(".png", ".txt").replace(".jpg", ".txt") for path in self.path2imgs]
			self.trans_params = trans_parms
			self.transform = transform
	def __len__(self):
		return len(self.path2imgs)
	def __getitem__(self, index):
		path2img = self.path2imgs[index%len(self.path2imgs)].rstrip()
		labels = None
		if os.path.exists(path2label):
			labels=np.loadtxt(path2label).reshape(-1, 5)
		if self.transform:
			img, labels = self.transform(img, labels, self.trans_params)
		return img, labels, path2img
#2. 下一步,为训练集创建一个CocoDataset类的实例
root_data = "./data/coco"
path2trainList=os.path.join(root_data, "trainvalno5k.txt")
coco_train = CocoDataset(path2trainList)
print(len(coco_train))
#117264
# 获取coco_train中的一个样本数据
img, labels, path2img = coco_train[1]
print("image size:", img.size, type(img))
print("labels shape:", labels.shape, type(labels))
print("labels \n", labels)
# image size: (640, 426) 
# labels shape: (2, 5) 
# labels
# [[23. 0.77 0.49 0.34 0.70]
#  [23. 0.19 0.90 0.21 0.13]]
#3. 为验证集创建一个CocoDataset类的实例
path2valList=os.path.join(root_data, "5k.txt")
coco_val = CocoDataset(path2valList, transform=None, trans_params=None)
print(len(coco_val))
# 5000
# 获取coco_val的一个样本
img, labels, path2img = coco_val[7]
print("image size:", img.size, type(img))
print("labels shape:", labels.shape, type(labels))
print("labels \n", labels)
# image size: (640, 426) 
# labels shape: (3, 5) 
# labels
# [[20. 0.57 0.59 0.74 0.90]
#  [20. 0.49 0.40 0.61 0.63]
#  [20. 0.89 0.40 0.21 0.93]]

#4. 显示来自于coco_train和coco_val中的一些样本
import matplotlib.pylab as plt
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from torchvision.transforms.functional import to_pil_image
import random
# 获取COCO类别名称列表
path2cocoNames="./data/coco.names"
fp=open(path2cocoNames,"r")
coco_names=fp.read().split("\n")[:-1]
print("number of classes:", len(coco_names))
print(coco_names)
# number of classese: 80
# ['person', 'bicycle', 'car', 'motorbike', ...]
# 定义一个rescale_bbox函数将缩放归一化边界框转换到原始图像大小
def rescale_bbox(bb, W, H):
	x,y,w,h=bb
	return [x*w, y*H, w*W, h*H]
# 定义show_img_bbox函数显示带有标注框的图像
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
fnt=ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf",16)
def show_img_bbox(img, targets):
	if torch.is_tensor(img):
		img=pil_to_image(img)
	if torch.is_tensor(targets):
		targets=targets.numpy()[:,1:]
	W,H=img.size
	draw = ImageDraw.Draw(img)
	for tg in targets:
		id_=int(tg[0])
		bbox=tg[1:]
		bbox=rescale_bbox(bbox,W,H)
		xc,yc,w,h=bbox
		color=[int(c) for c in COLORS[id_]]
		name=coco_names[id_]
		draw.rectangle(((xc-w/2,yc-h/2), (xc+w/2,yc+h/2)),outline=tuple(color),width=3)
		draw.text((xc-w/2,yc-h/2),name,font=fnt,fill=(255,255,255,0))
		plt.imshow(np.array(img))
# 在前面的代码片段中,如果传递给ImageFont.truetype的字体在您的计算机上不可用,您可以从draw.text中删除
# font=fnt。或者,您可以使用更常见的字体,如以下所示	
# fnt=ImageFont.truetype("arial.ttf",16)

#5. 调用show_img_bbox函数显示coco_train中的样例
np.random.seed(2)
rnd_ind = np.random.randint(len(coco_train))
img,labels,path2img=coco_train[rnd_ind]
print(img.size, labels.shape)

plt.rcParams["figure.figsize"]=(20, 10)
show_img_bbox(img,labels)
# (640,428) (2,5)

#6. 调用show_img_bbox函数显示coco_val中的样例
np.random.seed(0)
rnd_ind = np.random.randint(len(coco_val))
img,labels,path2img=coco_val[rnd_ind]
print(img.size, labels.shape)

plt.rcParams["figure.figsize"]=(20, 10)
show_img_bbox(img,labels)
#  (640,428) (3,5)


Pytorch基础知识(8)多目标检测_第2张图片

数据变换(数据增强)

在本节中,我们将定义一个转换函数和要传递给CocoDataset类的参数。

#1. 首先定义pad_to_square函数
def pad_to_square(img, boxes, pad_value=0, normalized_labels=True):
	w,h=img.size
	w_factor,h_factor=(w,h) if normalized_labels else (1,1)
	dim_diff=np.abs(h-w)
	pad1 = dim_diff//2
	pad2=dim_diff-pad1
	if h<w:
		left,top,right,bottom=0,pad1,0,pad2
	else:
		left,top,right,bottom=pad1,0,pad2,0
	padding=(left,top,right,bottom)

	img_padded = TF.pad(img, padding=padding, fill=pad_value)
	w_padded, h_padded = img_padded.size
	x1=w_factor * (boxes[:,1] - boxes[:,3]/2)
	y1=h_factor * (boxes[:,2] - boxes[:,4]/2)
	x2=w_factor * (boxes[:,1] + boxes[:,3]/2)
	y2=h_factor * (boxes[:,2] + boxes[:,4]/2)

	x1 += padding[0] # left
	y1 += padding[1] # top
	x2 += padding[2] # right
	y2 += padding[3] # bottom
	boxes[:,1]=((x1+x2)/2)/w_padded
	boxes[:,2]=((y1+y2)/2)/h_padded
	boxes[:,3] *=w_factor/w_padded
	boxes[:,4] *=w_factor/h_padded
	return img_padded, boxes
#2. 使用hflip函数实现图片水平翻转
def hflip(image, labels):
	image = TF.hflip(image)
	labels[:,1]=1.0-labels[:,1]
	return image, labels
#3. 定义transformer函数
def transformer(image, labels, params):
	if params["pad2square"] is True:
		image, labels=pad_to_square(image, labels)
	image=TF.resize(image, params["target_size"])
	if random.random()<params["p_hflip"]:
		image, labels=hflip(image, labels)
	image=TF.to_tensor(image)
	targets=torch.zeros((len(labels),6))
	targets[:,1:]=torch.from_numpy(labels)
	return image, targets
#4. 现在,为训练数据创建CocoDataset对象并传递transformer函数
trans_params_train={
	"target_size":(416,416),
	"pad2square":True,
	"p_hflip":1.0,
	"normalized_labels":True,
}
coco_train = CocoDataset(path2trainList,transform=transformer,trans_params=trans_params_train)
# 显示coco_train的一个样本
np.random.seed(2)
rnd_ind=np.random.randint(len(coco_train))
img,targets,path2img=coco_train[rnd_ind]
print("image shape:", img.shape)
print("labels shape:", targets.shape)

plt.rcParams["figure.figsize"]=(20, 10)
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
show_img_bbox(img,targets)
# image shape: torch.Size([3, 416, 416])
# labels shape: torch.Size([2, 6])
#5. 为验证数据创建CocoDataset对象并传递transformer函数
trans_params_val={
	"target_size":(416,416),
	"pad2square":True,
	"p_hflip":0.0,
	"normalized_labels":True,
}
coco_val= CocoDataset(path2valList,transform=transformer,trans_params=trans_params_val)
# 显示coco_val的一个样本
np.random.seed(0)
rnd_ind=np.random.randint(len(coco_val))
img,targets,path2img=coco_val[rnd_ind]
print("image shape:", img.shape)
print("labels shape:", targets.shape)

plt.rcParams["figure.figsize"]=(20, 10)
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
show_img_bbox(img,targets)
# image shape: torch.Size([3, 416, 416])
# labels shape: torch.Size([3, 6])

定义Dataloaders

在本节中,我们将定义训练和验证数据加载器,这样我们就可以从coco_train和coco_val获得小批量的数据。

#1. 为训练集定义Dataloader对象
from torch.utils.data import DataLoader
batch_size=8
train_dl=DataLoader(coco_train, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True,collate_fn=collate_fn)
# collate_fn定义如下:
def collate_fn(batch):
	imgs,targets,paths = list(zip(*batch))
	# 删除空的框
	targets = [boxes for boxes in targets if boxes is not None]
	for b_i, boxes in enumerate(targets):
		boxes[:,0]=b_i
	targets = torch.cat(targets,0)
	imgs = torch.stack([img for img in imgs])
	return imgs, targets, paths
# 显示train_dl中的一个mini-batch
torch.manual_seed(0)
for imgs_batch,tg_batch,path_batch in train_dl:
	break
print(imgs_batch.shape)
print(tg_batch.shape, tg_batch.dtype)
# torch.Size([8, 3, 416, 416])
# torch.Size([32, 6]) torch.float32
#2. 为验证集定义Dataloader对象
val_dl=DataLoader(coco_val,batch_size=batch_size,shuffle=False,num_workers=0,pin_memory=True,collate_fn=collate_fn)
# 显示val_dl中的一个mini-batch
torch.manual_seed(0)
for imgs_batch,tg_batch,path_batch in val_dl:
	break
print(imgs_batch.shape)
print(tg_batch.shape, tg_batch.dtype)
# torch.Size([8, 3, 416, 416])
# torch.Size([83, 6]) torch.float32

代码解析:
在“创建自定义COCO数据集”小节中,我们为COCO数据集创建了一个PyTorch数据集类。首先,导入需要的包,然后,定义CocoDataset类。这个类首先定义了带有三个输入的__init__函数:

  • path2listFile:一个字符串,表示包含图像列表的文本文件的位置
  • transform:定义各种变换的函数,如调整大小和转换为张量
  • trans_params:一个Python字典,定义变换参数,如目标图像大小
    在__init__函数中,我们读取文本文件并将图像列表加载到self.path2imgs中。该文件包含将在__getitem__函数中加载的图像的完整路径。然后,我们提取标签的完整路径。在__len__函数中,我们返回了数据集的长度。
    接下来,我们定义了__getitem__函数。这个函数有一个输入,即要加载的图像的索引。我们分别从self.path2imgs和self.path2labels获得了图像和标签的完整路径。然后,我们以PIL对象的形式加载图像,以numpy数组的形式加载标签。最后,我们返回图像、标签和图像的完整路径。如果没有应用变换函数,返回的图像和标签是PIL和numpy对象。

在步骤2中,我们使用“trainvalno5k.txt”中的训练数据列表创建了CocoDataset类的对象。我们没有传递一个变换函数。如我们所见,coco_train包含用于训练的117264张图像。然后,我们从coco_train获得一个示例图像,并打印图像的大小、类型和标签。如预期的那样,返回了PIL图像和numpy数组。检查打印的标签。每一行的第一个数字是对象ID,而接下来的四个数字是规范化的边界框坐标。

在步骤3中,与步骤2类似,我们使用“5k.txt”中的验证数据列表创建了CocoDataset类的对象。正如我们所看到的,coco_val中有5000张图片。然后,我们从coco_val中获得一个样本示例,并打印图像和标签的类型和大小。您可以尝试通过更改索引来获取不同的图像。正如我们所见,没有任何转换,图像有不同的尺寸大小。

在第4步中,我们显示了来自coco_train和coco_val数据集的示例图像和对象边界框。首先,我们导入所需的包。然后,我们从cocoa .names文件中将COCO对象名称加载到一个列表中。正如预期的那样,该文件中有80个对象类别。我们将使用这些名称在边界框上显示对象名称。

接下来,我们定义了rescale_bbox辅助函数来将标准化的边界框缩放到原始图像大小。同样,为了以不同的颜色显示边界框,我们定义了COLORS,这是一个随机元组数组。

然后,我们定义了有两个输入的show_img_bbox辅助函数:

  • img:可以是PIL图像或PyTorch张量,其形状为3HW。
  • targets:边框坐标。它可以是形状为n5的numpy数组,也可以是形状为n6的PyTorch张量。
    在函数中,我们检查输入数据类型。如果输入是PyTorch张量,我们将它们转换为PIL图像和numpy数组。如果img是张量,我们使用torchvision的to_pil_image函数将它转换为PIL图像。如果targets是一个张量,则使用.numpy()方法将其转换为numpy数组,并跳过第二维度的第一个索引。

在辅助函数的其余部分中,我们循环了边界框并将它们添加到图像中。注意,我们分别从COLORS和names(基于对象索引)中获得了边界框的颜色和名称。名称作为一段文本放在边框的左上角。

接下来,我们调用show_img_bbox函数来显示来自coco_train和coco_val示例图像及其边界框。您可以注释掉np.random.seed行,以便在每次重新运行时看到不同的图像。

在"数据变换"小节中,我们定义了数据变换所需的函数。例如调整图像大小、数据增强或将数据转换为PyTorch张量。

我们首先定义了pad_to_square函数,它有四个输入:

  • img:一张PIL图像
  • boxes:一个numpy数组,形状为(n, 5),包含n个边界框
  • pad_value:像素填填充,默认为零
  • normalized_labels:显示边界框是否归一化到范围[0,1]的标志
    这个函数获取一个PIL图像并填充其边框,使其成为一个方形图像。在函数中,我们得到了图像的大小和标签的比例因子。然后,我们计算填充大小,并将其分为两个值pad1和pad2。例如,如果填充大小是100,那么我们有pad1= pad2= 50。但是如果填充大小是101,那么我们得到pad1=50, pad2=51。

然后,我们计算图像每边的填充大小。如果高度小于宽度,我们只需要在图像的顶部和底部添加像素。否则,我们将在图像的左边和右边添加像素。

在填充图像之后,我们根据填充大小调整边界框坐标。为此,我们提取了填充前的左上角的x1, y1和右下角的x2, y2坐标。

方框数组的格式为[id, xc, yc, w, h],其中id为对象标识符,xc, yc为边界框的中心坐标,w, h为边界框的宽度和高度。

然后,我们通过添加填充大小来调整x1, y1, x2, y2。接下来,我们使用调整后的x1、y1、x2、y2值计算边界框。注意,我们再次将标签规范化为[0,1]的范围。这个函数返回一个方形的PIL图像和一个numpy数组的标签。

在步骤2中,我们定义了hflip函数来水平翻转图像和标签。

在步骤3中,我们定义了带有三个输入的transformer函数:

  • image:一张PIL图像
  • labels:numpy数组的边界框,大小为(n, 5)
  • params:包含变换参数的Python字典

该函数输入PIL图像及其标签,并将变换后的图像和标签作为PyTorch张量返回。在函数中,我们检查pad2square标志,如果它是True,我们调用pad_to_square函数。然后,图像的大小调整为416416,这是一个yolov3网络的标准输入大小。接下来,我们调用hflip函数随机翻转图像以增强数据。最后,我们使用torchvision的to_tensor函数将PIL图像转换为PyTorch张量。标签也被转换为大小为n6的PyTorch张量。在小批量处理中,额外的维度将用于索引图像。

在第4步中,我们重新定义了coco_train;但是,这一次,我们将transformer和trans_params_train传递给CocoDataset类。为了强制水平翻转,我们将p_hflip概率设置为1.0。在实践中,我们通常将概率设为0.5。您可以在示例图像上看到转换的效果。图像从顶部和底部被填充为零,大小调整为416*416,并水平翻转。

类似地,在步骤5中,我们重新定义了coco_val。我们不需要对验证数据进行数据扩充,所以我们将p_hflip的概率设置为0.0。看看变换后的样本尺寸大小。图像从顶部和底部被填充为零,大小调整为416*416,但没有翻转。

在定义Dataloader小节中,我们分别为训练和验证数据集定义了Dataloader类的两个对象,train_dl和val_dl。我们还定义了collate_fn函数来处理小批量数据并返回PyTorch张量。该函数作为Dataloader类的参数给出,以便进程动态进行。在这个函数中,我们使用zip(*iterateble)对小批处理中的图像、目标和路径进行分组。然后,我们删除目标中的任何空边界框。接下来,我们在小批量处理中设置样本索引。最后,我们将图像和目标连接为PyTorch张量。为了了解它是如何工作的,我们从train_dl和val_dl中提取了一个小批处理,并打印了返回张量的形状。注意,在train_dl中批大小被设置为16,而在val_dl中被设置为32。此外,在train_dl的小批量处理中有87个边界框,但在val_中有250个边界框。

构建YOLOV3模型

YOLO-v3网络由stride=2、跳过连接和上采样层的卷积层构成。没有池层。网络接收一个大小为416416的图像作为输入,提供三个YOLO输出,如下图所示:
Pytorch基础知识(8)多目标检测_第3张图片
该网络对输入图像下采样32倍,得到尺寸为13
13的feature map,得到yolo-out1。为了提高检测性能,1313 feature map被向上采样到2626和52*52,分别得到yolo-out2和yolo-out3。feature map中的一个单元格预测了三个预定义锚点边界框。因此,网络总计预测13x13x3+26x26x3+52x52x3=10647个边界框。
一个边界框用85个数字定义:

  • 四个坐标,[x,y,w,h]
  • 一个对象得分
  • C=80类预测对应COCO数据集中的80个对象类别

在本教程中,我们将向您展示如何使用PyTorch开发YOLO-v3模型。
我们将定义几个函数来解析配置文件,创建PyTorch模块,并定义Darknet模型。

myutils.py文件

#myutils.py
import torch
from torch import nn
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

def parse_model_config(path2file):
    cfg_file = open(path2file, 'r')
    lines = cfg_file.read().split('\n')

    lines = [x for x in lines if x and not x.startswith('#')]
    lines = [x.rstrip().lstrip() for x in lines] 
    
    blocks_list = []
    for line in lines:
        # start of a new block
        if line.startswith('['): 
            blocks_list.append({})
            blocks_list[-1]['type'] = line[1:-1].rstrip()
        else:
            key, value = line.split("=")
            value = value.strip()
            blocks_list[-1][key.rstrip()] = value.strip()

    return blocks_list


def create_layers(blocks_list):
    hyperparams = blocks_list[0]
    channels_list = [int(hyperparams["channels"])]
    module_list = nn.ModuleList()
    
    for layer_ind, layer_dict in enumerate(blocks_list[1:]):
        modules = nn.Sequential()
        
        if layer_dict["type"] == "convolutional":
            filters = int(layer_dict["filters"])
            kernel_size = int(layer_dict["size"])
            pad = (kernel_size - 1) // 2
            bn=layer_dict.get("batch_normalize",0)    
            
            
            conv2d= nn.Conv2d(
                        in_channels=channels_list[-1],
                        out_channels=filters,
                        kernel_size=kernel_size,
                        stride=int(layer_dict["stride"]),
                        padding=pad,
                        bias=not bn)
            modules.add_module("conv_{0}".format(layer_ind), conv2d)
            
            if bn:
                bn_layer = nn.BatchNorm2d(filters,momentum=0.9, eps=1e-5)
                modules.add_module("batch_norm_{0}".format(layer_ind), bn_layer)
                
                
            if layer_dict["activation"] == "leaky":
                activn = nn.LeakyReLU(0.1)
                modules.add_module("leaky_{0}".format(layer_ind), activn)
                
        elif layer_dict["type"] == "upsample":
            stride = int(layer_dict["stride"])
            upsample = nn.Upsample(scale_factor = stride)
            modules.add_module("upsample_{}".format(layer_ind), upsample) 
            

        elif layer_dict["type"] == "shortcut":
            backwards=int(layer_dict["from"])
            filters = channels_list[1:][backwards]
            modules.add_module("shortcut_{}".format(layer_ind), EmptyLayer())
            
        elif layer_dict["type"] == "route":
            layers = [int(x) for x in layer_dict["layers"].split(",")]
            filters = sum([channels_list[1:][l] for l in layers])
            modules.add_module("route_{}".format(layer_ind), EmptyLayer())
            
        elif layer_dict["type"] == "yolo":
            anchors = [int(a) for a in layer_dict["anchors"].split(",")]
            anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]

            mask = [int(m) for m in layer_dict["mask"].split(",")]
            
            anchors = [anchors[i] for i in mask]
            
            num_classes = int(layer_dict["classes"])
            img_size = int(hyperparams["height"])
            
            yolo_layer = YOLOLayer(anchors, num_classes, img_size)
            modules.add_module("yolo_{}".format(layer_ind), yolo_layer)
            
        module_list.append(modules)       
        channels_list.append(filters)

    return hyperparams, module_list        



class EmptyLayer(nn.Module):
    def __init__(self):
        super(EmptyLayer, self).__init__()
        
        
class YOLOLayer(nn.Module):

    def __init__(self, anchors, num_classes, img_dim=416):
        super(YOLOLayer, self).__init__()
        self.anchors = anchors
        self.num_anchors = len(anchors)
        self.num_classes = num_classes
        self.img_dim = img_dim
        self.grid_size = 0 
        
        
    def forward(self, x_in):
        batch_size = x_in.size(0)
        grid_size = x_in.size(2)
        devide=x_in.device
        
        prediction=x_in.view(batch_size, self.num_anchors, 
                             self.num_classes + 5, grid_size, grid_size)
        prediction=prediction.permute(0, 1, 3, 4, 2)
        prediction=prediction.contiguous()
        
        obj_score = torch.sigmoid(prediction[..., 4]) 
        pred_cls = torch.sigmoid(prediction[..., 5:]) 
        
        if grid_size != self.grid_size:
            self.compute_grid_offsets(grid_size, cuda=x_in.is_cuda)
            
        pred_boxes=self.transform_outputs(prediction) 
        
        output = torch.cat(
            (
                pred_boxes.view(batch_size, -1, 4),
                obj_score.view(batch_size, -1, 1),
                pred_cls.view(batch_size, -1, self.num_classes),
            ), -1,)
        return output        
    
    
    # 计算网格偏移
    def compute_grid_offsets(self, grid_size, cuda=True):
        self.grid_size = grid_size
        self.stride = self.img_dim / self.grid_size
        
        self.grid_x = torch.arange(grid_size, device=device).repeat(1, 1, grid_size, 1 ).type(torch.float32)
        self.grid_y = torch.arange(grid_size, device=device).repeat(1, 1, grid_size, 1).transpose(3, 2).type(torch.float32)
        
        scaled_anchors=[(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors]
        self.scaled_anchors=torch.tensor(scaled_anchors,device=device)
        
        self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1))
        self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1))
        
        
    # 将预测结果转换为边界框
    def transform_outputs(self,prediction):
        device=prediction.device
        x = torch.sigmoid(prediction[..., 0]) # Center x
        y = torch.sigmoid(prediction[..., 1]) # Center y
        w = prediction[..., 2] # Width
        h = prediction[..., 3] # Height

        pred_boxes = torch.zeros_like(prediction[..., :4]).to(device)
        pred_boxes[..., 0] = x.data + self.grid_x
        pred_boxes[..., 1] = y.data + self.grid_y
        pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w
        pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h
        
        return pred_boxes * self.stride        

解析配置文件

我们需要解析配置文件,以便能够构建模型。我们提供了一个myutils.py文件,其中包含一个辅助函数,您可以使用它来实现这一点。配置文件yolov3.cfg之前请下载yolov3.cfg 配置文件。

#1. 首先,我们导入parse_model_config辅助函数
from myutils import parse_model_config
#2. 使用parse_model_config函数读取和打印配置文件
path2config="./config/yolov3.cfg"
blocks_list=parse_model_config(path2config)
print(blocks_list)
# [{'type': 'net',
#   'batch': '1',
#   'subdivisions': '1',
#   'width': '416',
#   'height': '416',
#   ...

构建PyTorch模块

在本节中,我们将基于已解析的配置文件创建PyTorch模块。我们提供了myutils.py文件,其中包含一个可以用来创建YOLOv3网络的PyTorch模块的辅助函数。

#1. 首先,导入create_layer辅助函数,将blocks_list转换为PyTorch模块
from myutils import create_layers
#2. 调用create_layers函数,获得PyTorch模块的列表
hy_pa,m_l=create_layers(blocks_list)
print(hy_pa)
print(m_l)
# 打印超参数
# {'type': 'net',
# 'batch': '1',
# 'subdivisions': '1',
# 'width': '416',
# 'height': '416',
# 'channels': '3',
# ....
# 打印一些模块
# ModuleList(
# (0): Sequential(
#    (conv_0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1,1), bias=False)
#    (batch_norm_0): BatchNorm2d(32, eps=1e-05,momentum=0.9,affine=True,track_running_stats=True)
#    (leaky_0): LeakyReLU(negative_slope=0.1)
# )
# .....

定义Darknet模型

class Darknet(nn.Module):
	#1. 首先定义__init__函数
	def __init__(self, config_path, img_size=416):
		super(Darknet, self).__init__()
		self.blocks_list=parse_model_config(config_path)
		self.hyperparams, self.module_list=create_layers(self.blocks_list)
		self.img_size=img_size
	#2. 定义forward函数
	def forward(self, x):
		img_dim = x.shape[2]
		layer_outputs, yolo_outputs = [], []
		for block, module in zip(self.blocks_list[1:],self.module_list):
			if block["type] in ["convolutional", "upsample", "maxpool"]:
				x = module(x)
			elif block["type"] == "shortcut":
				layer_ind = int(block["from"])
				x = layer_outputs[-1] + layer_outputs[layer_ind]
			elif block["type"] == "yolo":
				x = module[0](x)
				yolo_outputs.append(x)
			elif block["type"] == "route":
				x = torch.cat([layer_outputs[int(l_i)] for l_i in block["layers"].split(",")],1)
			layer_outputs.append(x)
		yolo_out_cat = torch.cat(yolo_outputs, 1)
		return yolo_out_cat, yolo_outputs
#3. 创建Darknet类的实例对象
model = Darknet(path2config).to(device)
print(model)
# Darknet(
# (module_list): ModuleList(
# (0): Sequential(
# (conv_0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1),
# padding=(1, 1), bias=False)
# (batch_norm_0): BatchNorm2d(32, eps=1e-05, momentum=0.9,
# affine=True, track_running_stats=True)
# (leaky_0): LeakyReLU(negative_slope=0.1)
# ...
#4. 接下来,让我们使用一个虚拟输入来测试这个模型:
dummy_img = torch.rand(1,3,416,416).to(device)
with torch.no_grad():
	dummy_out_cat, dummy_out = model.forward(dummy_img)
	print(dummy_out_cat.shape)
	print(dummy_out[0].shape, dummy_out[1].shape, dummy_out[2].shape)
# torch.Size([1, 10647, 85])
# torch.Size([1, 507, 85]) torch.Size([1, 2028, 85]) torch.Size([1, 8112, 85])				

代码解析:
在解析配置文件小节中,我们定义了一个辅助函数来解析YOLO-v3配置文件。为了简短起见,我们在myutils.py中定义了辅助函数。函数的输入是path2file,一个字符串,可以在"./config/yolov3.cfg"中找到。该函数解析配置文件,并将层通过字典列表返回。在辅助函数中,我们使用.split("\n")将文件分割成列表。然后,我们删除了任何空行和注释。接下来,我们分别使用rstrip()和lstrip()删除一行的左右空白区域。接下来,我们定义了一个名为layers_list的空列表,并对列表进行循环。如果一行以“[”开头,一个新层被创建。对于新层,我们增加一个字典到layers_list。字典有一个“type”键,显示层的类型。第一层的类型是“net”,它定义了网络的超参数。我们将“height”和“width”参数设置为“416”。如果某一行没有以“[”开头,则作为层的属性添加到当前字典中。

在创建PyTorch模块小节中,我们定义了一个名为create_layers的辅助函数来创建对应于这些层的PyTorch模块。为了简短起见,我们在myutils.py文件中定义了辅助函数,并从myutils.py中导入了辅助函数。不过,我们将解释辅助函数。

函数的输入是blocks_list,它提供了解析配置文件小节中从配置文件中解析的层列表。
在函数中,我们从第一个block中提取超参数。接下来,我们创建了channels_list来保存每个层的输入通道数量。对于第一层,输入通道数为3(与图像通道相同)。接下来,我们创建了一个nn.ModuleList类的对象来保存子模块。然后,我们创建了一个循环来检查blocks列表。

接下来,我们创建了nn.Sequentia类的一个对象,将多个层添加到模块中。例如,对于“convolutional”块,我们使用add_module方法依次添加nn.Conv2d,nn.BatchNorm2d和nn.LeakyReLU。

接下来,在主循环中,我们检查了上采样块。在YOLO-v3中使用上采样层,将特征图上采样到更高的分辨率,以便与之前的层堆叠。nn.Upsample模块取stride=2参数,向上采样feature maps的倍数为2。我们使用默认模式="nearest"来进行上采样。

接下来,在循环中,我们检查了shortcut块。它们用于创建跳过连接。

配置文件中的shortcut块定义如下:
[shortcut]
from=-3
activation=linear
这里,from=-3表示跳过连接来自shortcut块从后(当前)向之前推的第三层。滤波器个数为channels_list中存储的跳过连接的滤波器个数。

接下来,在循环中,我们检查了route块。在配置文件中使用一个或两个参数定义了route块。

如果使用一个参数定义,如以下代码段所示,则返回第4层的特性映射(从路由层向后):
[route]
layers=-4
如果定义了两个参数,则它们用于连接来自多个层的特性映射。例如,在下面的代码片段中,来自前一层和第61层的特征映射沿着深度维度连接起来:
[route]
layers=-1,61
注意,实际的连接将发生在网络类的前向函数中。在这里,我们只汇总了路由层中的滤波器数量。

下一个循环,我们检查了yolo块。配置文件中有三个yolo块对应于三个检测输出。让我们看看配置文件的第一个yolo块:
[yolo]
mask = 6,7,8
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
在我们的代码中,我们提取了anchors属性,并将anchors分为一个由9个元组组成的列表,这些元组对应于YOLO-v3中的9个锚。第一个元组在索引0处,为(10,13),而最后一个元组在索引8处,为(373,326)。每个YOLO输出有三个anchors。

接下来,我们提取mask属性作为一个包含三个值的列表。因为每个yolo块有三个锚点,所以mask属性表示这三个锚点的索引。例如,索引在6,7,8的最后三个锚点属于第一个yolo输出。因此,第一个yolo输出的锚点如下: anchors = [(116,90), (156,198), (373,326)]

然后提取目标类别数和图像大小,分别为80和416。现在,我们检查了create_layers函数主循环中的所有层类型。每个层类型都会产生一个附加到module_list的PyTorch模块。然后,我们返回module_list和hyperparams。

接下来,我们定义了EmptyLayer类,它在跳过或shortcut连接中什么也不做。然后,我们定义了YOLOLayer类。首先,我们定义了YOLOLayer类的大部分,并逐个添加函数,以提高代码的可读性。然后,我们定义了__init__函数并初始化了变量。该函数有三个输入:

  • anchors,包含三个元组的列表
  • num_classes, COCO-2107数据集的80个类别
  • img_dim, 图像尺寸为416

接下来,在类中,我们定义了具有一个输入的前向函数。对于第一个yolo层,函数的输入是一个形状为[batch_size, 3*85, 13, 13]的特征图。该函数首先提取批处理大小和网格大小。对于第一个yolo层,网格大小是13。接下来,我们将输入特征图调整为[batch_size, 3, 13, 13, 85]。

接下来,我们对目标得分和类别预测应用sigmoid函数。如前所述,网络对每个网格的每个anchor预测85个值:4个值为[x, y, w, h],一个目标得分和80个类预测。然后,我们使用compute_grid_offsets函数计算网格偏移量。我们在第4步中定义了这个辅助函数。接下来,我们使用transform_outputs函数将预测转换为边界框,该函数在步骤5中定义。

然后,将边界框预测与object score和类别预测连接起来,作为函数的输出返回。

接下来,我们定义了compute_grid_offset辅助函数。辅助函数是YOLOLayer类的一个方法,有三个输入:

  • self, 这是指YOLOLayer类
  • grid_size:网格大小,可以是13、26、52。
  • flag: 值为True或False取决于GPU设备可用性 。
    对于给定的grid_size=13,该函数在x和y维度上创建一个13*13的网格。它还通过self.stride=32来缩放anchors。
    在第5步中,我们定义了transform_outputs辅助函数。这个函数是YOLOLayer类的一个方法,有两个输入:
  • self,这是指YOLOLayer类
  • predictions,形状为(batch_size, 3, grid_size, grid_size,85)的tensor
    这个函数应用了sigmoid函数,在x, y坐标上添加了网格偏移量,并使用缩放anchors将它们缩放到w,h。这个辅助函数的输出是缩放的边界框预测。

在步骤2中,我们调用了create_layers函数以确保它能正常工作。这个函数的输入是我们通过解析配置文件获得的blocks_list。该函数返回超参数和106个PyTorch模块的列表,这些模块对应于已解析的层。

在Defining the Darknet model小节中,我们定义了Darknet类,这是一个完整的检测网络。首先,我们定义了带有三个输入的__init__函数:

  • self,这里指Darknet类
  • config_path,配置文件的路径
  • img_size,图像尺寸大小,默认为416
    在__init__函数中,我们调用了parse_model_config和create_layers函数来获得blocks列表和
    PyTorch模块。这些是YOLO-v3网络的构建模块。
    在步骤2中,我们定义了具有两个输入的前向函数:
  • self,这里指Darknet类
  • x,形状为(batch_size, 3, 416, 416)的tensor
    在前向函数中,我们首先定义了两个列表来跟踪每一层的输出和yolo层的输出。然后,我们遍历了块和模块的列表。
    提醒一下,第一个块属于超参数。因此,在self.blocks_list[1:]中,索引从1开始。

在循环中,如果遇到 convolutional块,我们将输入张量传递给模块,并将输出添加到layer_outputs列表。然后,如果遇到shortcut块,我们提取shortcut层索引,将shortcut层的输出添加到上一层的输出。然后将结果添加到layer_outputs列表中。接下来,如果遇到yolo层,我们将把模块输出添加到layer_outputs和yolo_outputs列表。最后,如果遇到route层,我们提取route层索引并连接route层的输出。最后,我们将yolo_outputs列表中的张量连接为yolo_out_cat张量,并返回两个输出。

在步骤3中,我们创建了一个名为model的Darknet类对象并将其打印出来。正如我们所看到的,已经创建了完整的YOLO-v3架构。当模型被打印出来时,我们检查了它的架构。它应该有106个层,其中三个YOLOLayer模块的索引为82、94和106。

在YOLO-v3架构中总共有106个层。层82、94、106是三个yolo输出。

在步骤4中,为了检查结构并验证我们的代码,我们向模型传递了一个虚拟图像并获得了输出。提醒一下,有三个yolo输出的形状(1,507, 85)、(1,2028,85)和(1,8112,85)。最后的输出是维度1中这三个层的堆叠。输出大小为(1,10647,85)因为507 + 2028 + 8112 = 10647。换句话说,对于每个输入图像,YOLO-v3预测10647个边界框。

定义损失函数

在本教程中,我们将为YOLO-v3体系结构定义一个损失函数。要深入了解YOLO-v3损失,请回忆一下包含以下元素的模型输出:

  • 边界框[x,y,w,h]
  • object score
  • 80个对象类别的类别预测
    因此,yolov3的损失函数由以下部分组成:
    在这里插入图片描述
    Pytorch基础知识(8)多目标检测_第4张图片
    在本教程中,您将学习如何为YOLO-v3算法实现组合损失函数。
  1. 定义get_loss_batch函数来计算小批量数据的损失值
def get_loss_batch(output, targets, params_loss, opt=None):
	ignore_thres=params_loss["ignore_thres"]
	scaled_anchors=params_loss["scaled_anchors"]
	mse_loss = params_loss["mse_loss"]
	bce_loss = params_loss["bce_loss"]
	num_yolos=params_loss["num_yolos"]
	num_anchors=params_loss["num_yolos"]
	obj_scale=params_loss["obj_scale"]
	noobj_scale=params_loss["noobj_scale"]
	loss=0.0
	for yolo_ind in range(num_yolos):
		yolo_out=output[yolo_ind]
		batch_size, num_bbxs, _=yolo_out.shape
		gz_2=num_bbxs/num_anchors
		grid_size=int(np.sqrt(gz_2))
		yolo_out = yolo_out.view(batch_size, num_anchors, grid_size, grid_size, -1)
		# 提取预测的边界框
		pred_boxes=yolo_out[:,:,:,:,:4]
		x,y,w,h = transform_bbox(pred_boxes,scaled_anchors[yolo_ind])
		pred_conf=yolo_out[:,:,:,:,4]
		pred_clas_prob=yolo_out[:,:,:,:,5:]
		yolo_targets=get_yolo_targets({
			"pred_cls_prob":pred_cls_prob,
			"pred_boxes":pred_boxes,
			"targets":targets,
			"anchors":scaled_anchors[yolo_ind],
			"ignore_thres":ignore_thres,
		})
		obj_mask=yolo_targets["obj_mask"]
		noobj_msk=yolo_targets["noobj_mask"]
		tx=yolo_targets["tx"]
		ty=yolo_targets["ty"]
		tw=yolo_targets["tw"]
		th=yolo_targets["th"]
		tcls=yolo_targets["tcls"]
		t_conf=yolo_targets["t_conf"]
	
		loss_x = mse_loss(x[obj_mask], tx[obj_mask])
		loss_y = mse_loss(y[obj_mask], ty[obj_mask])
		loss_w = mse_loss(w[obj_mask], tw[obj_mask])
		loss_h = mse_loss(h[obj_mask], th[obj_mask])

		loss_conf_obj = bce_loss(pred_conf[obj_mask], t_conf[obj_mask])
		loss_conf_noobj = bce_loss(pred_conf[noobj_mask], t_conf[noobj_mask])
		loss_conf = obj_scale * loss_conf_obj + noobjscale * loss_conf_noobj
		loss_cls = bce_loss(pred_cls_prob[obj_mask], tcls[obj_mask])
		loss += loss_x + loss_y + loss_w + loss_h + loss_conf + loss+cls
	if opt is not None:
		opt.zero_grad()
		loss.backward()
		opt.step()
	return loss.item()

在step1中,调用了transform_bbox 函数,该函数定义如下

def transform_bbox(bbox, anchors):
	x=bbox[:,:,:,:,0]
	y=bbox[:,:,:,:,1]
	w=bbox[:,:,:,:,2]
	h=bbox[:,:,:,:,3]
	anchor_w=anchors[:,0].view((1,3,1,1))
	anchor_h=anchors[:,1].view((1,3,1,1))
	x=x-x.floor()
	y=y-y.floor()
	w=torch.log(w/anchor_w+1e-16)
	h=torch.log(h/anchor_h+1e-16)
	return x,y,w,h
  1. 在step1中,调用了get_yolo_targets函数,该函数定义如下:
def get_yolo_targets(params):
	pred_boxes=params["pred_boxes"]
	pred_cls_prob=params["pred_cls_prob"]
	target=params["targets"]
	anchors=params["anchors"]
	ignore_thres=params["ignore_thres"]

	batch_size=pred_boxes.size(0)
	num_anchors=pred_boxes.size(1)
	grid_size=pred_boxes.size(2)
	num_cls=pred_cls_prob.size(-1)

	sizeT=batch_size, num_anchors, grid_size, grid_size
	obj_mask=torch.zeros(sizeT, device=device,dtype=torch.uint8)
	noobj_mask=torch.zeros(sizeT,device=device,dtype=torch.uint8)
	tx=torch.zeros(sizeT,device=device,dtype=torch.float32)
	ty=torch.zeros(sizeT,device=device,dtype=torch.float32)
	tw=torch.zeros(sizeT,device=device,dtype=torch.float32)
	th=torch.zeros(sizeT,device=device,dtype=torch.float32)
	sizeT=batch_size, num_anchors,grid_size,grid_size,num_cls
	tcls=torch.zeros(sizeT,device=device,dtype=torch.float32)
	# 缩放和提取目标边界框
	target_bboxes=target[:,2:]*grid_size
	t_xy=target_bboxes[:,:2]
	t_wh = target_bboxes[:,2:]
	t_x, t_y=t_xy.t()
	t_w, t_h=t_wh.t()
	
	grid_i, grid_j = t_xy.long().t()
	# 选择与targets有最高IOU的anchor
	iou_with_anchors = [get_iou_WH(anchor, t_wh) for anchor in anchors]
	iou_with_anchors = torch.stack(iou_with_anchors)
	best_iou_wa, best_anchor_ind=iou_with_anchors.max(0)
	# 设置objext mask tesnors
	batch_inds, target_labels = target[:,:2].long().t()
	obj_mask[batch_inds, best_anchor_ind, grid_j, grid_i] = 1
	noobj_mask[batch_inds, best_anchor_ind, grid_j, grid_i] = 0
	for ind, iou_wa in enumerate(iou_with_anchors.t()):
		noobj_mask[batch_inds[ind], iou_wa>ignore_thres, grid_j[ind], grid_i[ind]] = 0
	# 设置x和y
	tx[batch_inds, best_anchor_ind, grid_j, grid_i] = t_x - t_x.floor()
	ty[batch_inds, best_anchor_ind, grid_j, grid_i] = t_y - t_y.floor()
	# 设置w和h
	anchor_w = anchors[best_anchor_ind][:,0]
	tw[batch_inds, best_anchor_ind, grid_j, grid_i] = torch.log(t_w/anchor_w + 1e-16)
	th[batch_inds, best_anchor_ind, grid_j, grid_i] = torch.log(t_h/anchor_h + 1e-16)
	# 设置target类别
	tcls[batch_inds, best_anchor_ind, grid_j, grid_i, target_labels] = 1
	# 最后,以Python字典的形式返回输出
	output = {
		"obj_mask":obj_mask,
		"noobj_mask":noobj_mask,
		"tx":tx,
		"ty":ty,
		"tw":tw,
		"th":th,
		"tcls":tcls,
		"t_conf":obj_mask.float(),
	}
	return output
  1. 定义get_iou_wh辅助函数
def get_iou_WH(wh1, wh2):
	wh2 = wh2.t()
	w1,h1=wh1[0],wh1[1]
	w2,h2=wh2[0],wh2[1]
	inter_area=torch.min(w1,w2) * torch.min(h1,h2)
	union_area=(w1*h1 + 1e-16) + w2*h2 - inter_area
	return inter_area/union_area

代码解析
在第1步中,我们定义了get_loss_batch函数。函数输入如下:

  • output: 对应于YOLO-v3输出的三个张量的列表。
  • targets: 真实值,一个形状n*6的张量,其中n是这批数据中边界框的总数。
  • params_loss: 一个Python字典,它包含损失参数。
  • opt: 优化器类的对象。默认值为None。

最初,我们从params_loss字典中提取参数。然后,我们创建了一个num_yolos=3次迭代的循环。在每次迭代中,我们提取了YOLO输出(yolo_out)、边界框的数量(num_bboxes)和网格大小(gird_size)。

提醒一下,有三个YOLO输出,每个输出中分别有507、2028和8112个边界框。另外,网格的大小分别是13,26和52。

然后,在每次迭代中,我们将yolo_out调整为(batch_size, 3, grid_size, grid_size, 85)。这里,31313=507,32626=2028,35252=8112。根据调整后的张量,我们得到了预测的边界框(pred_boxes),目标得分(pred_conf)和类别概率(pred_cls_prob)。我们还使用transform_bbox辅助函数转换了预测的边界框。我们在步骤2中定义了transform_bbox辅助函数。

接下来,我们将params_target字典传递给get_yolo_targets函数以获得yolo_targets。我们在步骤3中定义了get_yolo_targets函数。get_yolo_targets函数的输出是一个字典,其中包含计算损失值所需的变量。

接下来,我们计算边界框的预测坐标和目标坐标之间的均方误差(mse_loss),(loss_x, loss_y, loss_w, loss_h)。然后,我们计算了预测和目标对象得分之间的二元交叉熵损失(bce_loss)。我们还计算了预测的类概率和目标标签的bce_loss。最后将计算出的所有损失值相加。这重复了三次,每次迭代的损失值都添加到前一次迭代的损失值上。
如果优化对象opt有值,我们计算梯度并执行优化步骤;否则,我们返回总体损失。优化器将在训练期间更新模型参数。

在步骤2中,我们定义了transform_bbox辅助函数。辅助函数的输入如下:

  • bbox: 一个形状为(batch_size, 3, grid_size, grid_size, 4)的张量,它包含预测的边界框。
  • anchors: 形状为(3,2)张量,包含每个YOLO输出的三个anchors的缩放后的宽度和高度。例如,在第一个YOLO输出中,三个anchor的宽度和高度分别是[[116,90],[156,198],[373, 326]],其缩放后的宽度和高度为[[3.6250,2.8125],[4.8750,6.1875],[11.6562, 10.1875]]。这里,比例因子是32。

该函数接受预测的边界框并对其进行转换,以便它们与目标值匹配。这个变换与我们在创建YOLOv3模型教程中定义的YOLOLayer类的transform_outputs函数相反。

在函数中,我们从bbox中提取了x,y,w,h张量。然后,我们将anchors张量的两列进行切片,并使用.view()方法将其形状调整为(1,3,1,1)。接下来,我们转换这些值并返回分别对应于x, y, w, h的一个包含四个张量的列表。

在第3步中,我们定义了get_yolo_targets辅助函数。该函数的输入是一个Python字典,包含以下键:

  • pred_boxes:包含预测边框,形状为(batch_size, 3, grid_size, grid_size,4)的tensor
  • pred_cls_prob:包含预测类别概率,形状为(batch_size, 3, grid_size, grid_size,80)的tensor
  • targets:一个包含真实边界框和标签,形状为(n,6)tensor,其中n表示每个mini-batch中边界框的数目
  • anchors:一个包含缩放后三个anchors的宽和高,形状为(2,3)的tensor
  • ignore_thres:阈值,值为0.5

在函数中,我们提取了键值并初始化了输出张量。obj_mask和noobj_mask张量将在稍后计算损失函数时用于过滤边界框的坐标。然后从目标张量中分割出目标边界框,并按网格大小进行缩放。这是由于原来的目标边界框是标准化的。形状为(n,4)的新的张量target_bboxes包含了边界框[x, y, w, h]的缩放后的坐标。接下来,我们将x, y, w, h提取成n个四个单独的张量。

.t()方法用于对一个张量进行转置。例如,如果A是一个2x3张量,那么A.t()就是一个3x2张量。

接下来,我们使用get_iou_WH 函数计算目标和三个anchors的IoU。然后,我们找到与target的IOU最高的anchor。get_iou_WH函数是在第4步中定义的。

.long()方法用于将浮点值转换为整数值。

接下来,我们从target张量中提取批索引和对象标签。batch_inds变量保存批处理中图像的索引,范围从0到batch_size-1。target_labels变量保存对象类别,范围从0到79,因为对象类的总数是80。接下来,我们更新了obj_mask、noobj_mask、tx、ty、tw、th和tcls张量。

tx和ty张量被更新,以便它们能够保存范围为(0,1)的值。

在第4步中,我们定义了get_iou_WH函数。函数的输入如下:

  • wh1:包含anchor的宽和高,形状为(1,2)
  • wh2:包含target边界框的宽和高,形状为(n,2)
    这里,我们把wh2转置成一个形状为(2,n)的张量。然后,计算target与anchor的交集和并集,并返回IOU值。

训练模型

到目前为止,我们已经创建了数据加载器并定义了模型和损失函数。在这个教程中,我们将编写代码来训练模型。训练过程将遵循标准的随机梯度下降(SGD)过程。我们将在训练数据上训练模型,并在验证数据上评估它。由于COCO数据集较大和我们的深度模型的YOLO-v3架构,训练将非常缓慢,即使使用GPU。您可能需要训练模型长达一周,以获得良好的性能。
在本教程中,我们将定义一些辅助函数,设置必要的超参数,并训练模型。

#1. 定义loss_epoch函数来计算每个epoch的损失值
def loss_epoch(model,params_loss,dataset_dl,sanity_check=False,opt=None):
	running_loss=0.
	len_data=len(dataset_dl.dataset)
	running_metrics={}
	for xb,yb,_ in dataset_dl:
		yb=yb.to(device)
		_,output=model(xb.to(device))
		loss_b=get_loss_batch(output,yb,params_loss,opt)
		running_loss+=loss_b
		if sanity_check is True:
			break
	loss=running_loss/float(len_data)
	return loss

#2. 定义train_val来训练模型
import copy
def train_val(model,params):
	num_epochs=params["num_epochs"]
	params_loss=params["params_loss"]
	opt=params["optimizer"]
	train_dl=params["train_dl"]
	val_dl=params["val_dl"]
	sanity_check=params["sanity_check"]
	lr_scheduler=params["lr_scheduler"]
	path2weights=params["path2weights"]
	loss_history={
		"train":[],
		"val":[],
	}
	best_model_wts=copy.deepcopy(model.state_dict())
	best_loss=float("inf")
	for epoch in range(num_epochs):
		current_lr=get_lr(opt)
		print("Epoch {}/{}, current lr={}".format(epoch, num_epochs-1,current_lr))
		model.train()
		train_loss=loss_epoch(model,params_loss,train_dl,sanity_check,opt)
		loss_history["train"].append(train_loss)
		print("train loss:%.6f"%(train_loss))
		
		model.eval()
		with torch.no_grad():
			val_loss=loss_epoch(model,params_loss,val_dl,sanity_check)
			loss_history["val"].append(val_loss)
			print("val loss:%.6f"%(val_loss))
			if val_loss<best_loss:
				best_loss=val_loss
				best_model_wts=copy.deepcopy(model.state_dict())
				torch.save(model.state_dict(),path2weights)
				print("Copied best model weights!")
			lr_scheduler.step(val_loss)
			if current_lr != get_lr(opt):
				print("Loading best model weights!")
				model.load_state_dict(best_model_wts)
		print("-"*10)
	model.load_state_dict(best_model_wts)
	return model,loss_history
def get_lr(opt):
	for param_group in opt.param_groups:
		return params_group["lr"]
#3. 在调用train_val函数之前,定义优化器
from torch import optim
from torch.optim.lr_scheduler import ReduceLROnPlateau

opt=optim.Adam(model.parameters(),lr=1e-3)
lr_scheduler=ReduceLROnPlateau(opt, mode="min",factor=0.5,patience=20,verbose=1)
path2models="./models/"
if not os.path.exists(path2models):
	os.mkdir(path2models)
scaled_anchors=[model.module_list[82][0].scaled_anchors,
				model.module_list[94][0].scaled_anchors,
				model.module_list[106][0].scaled_anchors]
# 设置损失参数
mse_loss = nn.MSELoss(reduction="sum")
bce_loss = nn.BCELoss(reduction="sum")
param_loss={
	"scaled_anchors": scaled_anchors,
	"ignore_thres":0.5,
	"mse_loss":mse_loss,
	"bce_loss":bce_loss,
	"num_yolos":3,
	"num_anchors":3,
	"obj_scale":1,
	"noobj_scale":100,
	}
# 设置训练参数并训练模型
params_train = {
	"num_epochs":20,
	"optimizer":opt,
	"params_loss":params_loss,
	"train_dl":train_dl,
	"val_dl":val_dl,
	"sanity_check":False,
	"lr_scheduler":lr_scheduler,
	"path2weights":path2models+"weights.pt",
}
model,loss_hist=train_val(model, params_train)
# 如果您的计算机出现内存不足错误(OOM),请尝试减少批处理大小并重新启动笔记本。
# Epoch 0/99, current lr=0.001
# train loss: 2841.816801
# val loss: 689.142467
# Copied best model weights!
# ----------
# Epoch 1/99, current lr=0.001
# train loss: 699.876479
# val loss: 659.404275
# Copied best model weights!
# ----------

代码解析:
在第1步中,我们定义了loss_epoch 函数来计算每个epoch中的损失值。
这个函数的输入如下:

  • model:一个模型类对象
  • params_loss:保存损失函数的参数的Python字典
  • dataset_dl:数据加载器类的对象
  • sanity_check:默认值为False的标志,用于在一次批处理之后中断循环
  • opt:优化器类的对象,默认值为None

在这个函数中,我们使用数据加载器提取成批的数据和目标值(target)。然后,我们将数据输入模型并获得两个输出。
模型返回两个输出。第一个输出是一个形状为(batch_size, 10647, 85)张量,这是三个YOLO输出的堆叠。第二个输出是三个张量的列表,它们对应于三个YOLO输出。

接下来,我们使用get_loss_batch函数计算每个批处理的损失值,该函数在定义损失函数教程中定义。如果sanity_check标志为True,则中断循环;否则,我们继续循环并返回平均损失值。

在步骤2中,我们定义了train_val函数。函数的输入如下:

  • model:模型类对象
  • params:包含训练参数的Python字典

在这个函数中,我们提取了params键并初始化了一些变量。然后,训练循环开始了。在循环的每次迭代中,我们在训练数据上对模型进行一个epoch的训练,并使用loss_epoch函数计算每个epoch的训练损失。接下来,我们在验证数据上评估模型并计算验证损失。注意,对于验证数据,我们没有将opt参数传递给loss_epoch函数。如果验证损失在迭代中得到改善,那么我们保存一个模型权重的副本。我们还使用学习策略来监控损失,并在出现平坦(大于patience次epoch的损失不降低)的情况下降低学习率。最后,返回训练后的模型和损失历史。

我们还定义了get_lr辅助函数,这是一个简单的函数,它在训练循环的每次迭代中返回学习率。

在步骤3中,我们准备对模型进行训练。首先,我们定义了优化器和学习率计划。然后,我们定义了损失和训练参数。接下来,我们调用了train_val函数。由于庞大的COCO数据集和YOLO-v3模型,即使使用GPU,训练也会非常慢。你需要训练这个模型长达一周。最好先将sanity_check标志设置为True,以便在短时间内快速运行训练循环并修复任何可能的错误。然后,您可以将标志设置回False,并使用完整的数据集训练模型。

模型部署

现在训练已经完成,是时候部署模型了。要部署模型,我们需要定义模型类,如创建YOLOv3模型教程中所述。然后,我们需要将训练过的权重加载到模型中,并将其部署到验证数据集上。
让我们学习如何做这个。

#1.导入模型权重
path2weights="./models/weights.pt"
model.load_state_dict(torch.load(path2weights))
#2.从验证集中得到一个样本
img,tg,_=coco_val[4]
print(img.shape)
print(tg.shape)
# torch.size([3,416,416])
# torch.size([2,6])

#3.显示带有边界框的图像
show_img_bbox(img,tg)
#4.将图像传入模型得到输出结果
model.eval()
with torch.no_grad():
	out,_=model(img.unsqueeze(0).to(device))
print(out.shape)
# torch.size([1,10647,85])
#5. 将模型输出送入NonMaxSuppression函数
img_size=416
out_nms=NonMaxSuppression(out.cpu())
print(out_nms[0].shape)
# torch.Size([1,6])
#6. 显示带有预测框的图像
show_img_bbox(img,out_nms[0])
#7.NonMaxSuppression函数定义如下:
def NonMaxSuppression(bbox_pred, obj_threshold=0.5, nms_thres=0.5):
	bbox_pred[...,:4] = xywh2xyxy(bbox_pred[...,:4])
	output = [None] * len(bbox_pred)
	for ind, bb_pr in enumerate(bbox_pred):
		bb_pr = bb_pr[bb_pr[:,4] >=obj_threshold]
		if not bb_pr.size(0):
			continue
		score = bb_pr[:,4]*bb_pr[:,5:].max(1)[0]
		bb_pr=bb_pr[(-score).argsort()]
		cls_probs, cls_preds = bb_pr[:,5:].max(1,keepdim=True)
		detections = torch.cat((bb_pr[:,:5],cls_probs.float(),cls_preds.float()),1)
		bbox_nms=[]
		while detections.size(0):
			high_iou_inds = bbox_iou(detections[0,:4].unsqueeze(0), detections[:,:4])>nms_thres
			cls_match_inds=detections[0,-1]==detections[:,-1]
			supp_inds = high_iou_inds & cls_match_inds
			ww = detections[supp_inds, 4]
			detections[0,:4] = (ww.view(-1,1) * detections[supp_inds, :4]).sum(0)/ww.sum()
			bbox_nms += [detections[0]]
			detections = detections[~supp_inds]
		if bbox_nms:
			output[ind] = torch.stack(bbox_nms)
			output[ind] = xyxyh2xywh(output[ind])
	return output
#8.xywh2xyxy定义如下:
def xywh2xyxy(xywh):
	xyxy = xywh.new(xywh.shape)
	xyxy[...,0] = xywh[...,0] - xywh[...,2] / 2.0
	xyxy[...,1] = xywh[...,1] - xywh[...,3] / 2.0
	xyxy[...,2] = xywh[...,0] - xywh[...,2] / 2.0
	xyxy[...,3] = xywh[...,1] - xywh[...,3] / 2.0
#9.xyxyh2xywh定义如下:
def xyxyh2xywh(xyxy, image_size=416):
	xywh = torch.zeros(xyxy.shape[0],6)
	xywh[:,2] = (xyxy[:,0] + xyxy[:,2])/2./img_size
	xywh[:,3] = (xyxy[:,1] + xyxy[:,3])/2./img_size
	xywh[:,4] = (xyxy[:,2] - xyxy[:,0])/img_size
	xywh[:,5] = (xyxy[:,3] - xyxy[:,1])/img_size
	xywh[:,1] = xyxy[:,6)
	return xywh
#10. bbox_iou定义如下:
	def bbox_iou(box1, box2):
		b1_x1,b1_y1,b1_x2,b1_y2 = box1[:,0], box1[:,1],box1[:,2],box1[:,3]
		b2_x1,b2_y1,b2x2,b2_y2 = box2[:,0], box2[:,1],box2[:,2],box2[:,3]
		inter_rect_x1=torch.max(b1_x1,  b2_x1)
		inter_rect_y1=torch.max(b1_y1,  b2_y1)
		inter_rect_x2=torch.min(b1_x2,  b2_x2)
		inter_rect_y2=torch.min(b1_y2,  b2_y2)
		
		inter_area = torch.clamp(inter_rect_x2-inter_rect_x1 +1, min=0) * torch.clamp(inter_rect_y2 - inter_rect_y1 + 1, min=0)
		b1_area = (b1_x2 - b1_x1 + 1.0) * (b1_y2 - b1_y1 + 1.0)
		b2_area = (b2_x2 - b2_x1 + 1.0) * (b2_y2 - b2_y1 + 1.0)
		iou = inter_area / (b1_area + b2_area - inter_area + 1e-16)
		return iou

代码解析:

在步骤1中,我们将训练的权重加载到YOLOv3模型中。

在步骤2中,我们从验证集中选择一张图片,并且打印图像及其target形状。图像尺寸为(3,416,416), target形状为(2, 6),表示两个边界框。
target张量包含[img_ind, obj_id, x, y, w, h],其中img_ind是批量图像的索引,obj_id是对象标签。

在步骤3中,我们使用show_img_bbox辅助函数显示图像和相应的边界框。

在步骤4中,我们将模型设置为eval()模式,并将图像输入模型,得到第一个输出,打印输出形状。正如我们看到的,有10647个检测到的边界框。然而,并非所有检测到的边界框都是有效的,它们应该被过滤。步骤5使用NonMaxSuppression进行过滤。函数只返回一个有效的边界框。我们在第6步中显示了预测的边界框。似乎模型只能识别出图像中的人。

在第7步中,我们定义了NonMaxSuppression函数。简而言之,非最大抑制算法选择概率最高的边界框,并删除与所选边界框有较大重叠的任何边界框。

函数的输入如下:

  • bbox_pred:来自于模型输出,形状为(batch_size, 10647, 85)
  • obj_threshold:常量,我们比较每个边界框的预测目标得分的阈值
  • nms_threshold:常量,IOU阈值

在NonMaxSuppression 函数中,我们将[xc, yc, w, h]格式的预测边界框转换为[x1, y1, x2, y2]格式。然后,我们创建一个循环来读取每个图像的边界框。因此,bb_pr保存每个图像检测到的边界框,其形状为(10647, 85)。接下来,我们将bb_pr索引4处的预测对象评分与obj_threshold进行比较,以过滤出概率较低的边界框。接下来,我们将对象概率与最大类别概率相乘,称之为score。然后,我们根据score对剩下的边界框进行排序(降序)。接下来,我们计算索引0处的最高得分边界框和其他边界框之间的IOU,并找到大于nms_threshold的索引(high_iou_inds)。我们还找到了具有相同类预测的边界框的索引(cls_match_inds)。high_iou_inds和cls_match_inds的交集保存了具有相同类预测和高重叠(被抑制)的边界框的索引。最后,我们将过滤后的边界框从[x1, y1, x2, y2]转换为[xc, yc, w, h]格式,并返回输出。

在步骤8中,我们定义了xywh2xyxy函数。函数的输入是xywh,一个形状(n, 4)的张量,包含n个[xc, yc, w, h]格式的边界框。该函数返回一个形状为(n, 4)的张量,包含n个[x1, y1, x2, y2]格式的边界框。

在第9步中,我们定义了xyxy2xywh函数。函数输入是xyxy,一个形状为(n, 4)的张量,包含n个[x1, y1, x2, y2]格式的边界框。这个函数返回一个张量(n, 4),它包含n个[xc, yc, w, h]格式的边界框。

在第10步中,我们定义了bbox_iou辅助函数。该函数的输入如下:

  • box1:一个形状为(1,4)的张量,它包含一个[x1, y1, x2, y2]格式的边界框
  • box2:一个形状为(n,4)的张量,包含n个[x1, y1, x2, y2]格式的边界框

在这个函数中,我们计算了交集的面积。然后,我们计算并集的面积并返回IOU。

Pytorch基础知识(8)多目标检测_第5张图片

你可能感兴趣的:(PyTorch,目标检测,pytorch,深度学习)