前言
写这篇文章的初衷是为了记录自己在YOLOv3学习过程中的一些理解。在YOLOv3的学习过程中,我会有这种苦恼:网络的整体框架我能够明白,但是具体的实现过程却很模糊,于是便有了这篇博客。之前在学习YOLOv1时,这两篇博客对我的帮助很大:
经典论文解析——YOLOv1——目标检测
动手学习深度学习pytorch版——从零开始实现YOLOv1
如果对YOLO系列还没有大致的了解,建议先从这两篇博客入手学习。可惜博主没有更新后来的YOLO系列,于是我选择精读大神的PyTorch复现版本:
eriklindernoren/PyTorch-YOLOv3
本文就是根据这个版本的复现对YOLOv3算法训练部分的实现细节进行分析,面向的是对YOLOv3已经有大致了解并想研究训练部分具体实现方式的读者。如有错误,请评论指出,共同进步,谢谢。
一、数据预处理
代码是基于COCO数据集的复现,由于我之前用过VOCDeteciton2012的数据集,所以就继续使用VOC数据集了。
datasets.py和transforms.py包括了主要的数据集的读取和预处理。在datasets.py中创建了ListDataset类来封装数据,train.py中读取数据集的代码如下:
# Get dataloader
dataset = ListDataset(train_path, multiscale=True, transform=AUGMENTATION_TRANSFORMS)
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=8,
pin_memory=True,
collate_fn=dataset.collate_fn,
)
其中AUGMENTATION_TRANSFORMS的定义在transforms.py中,主要包括boxes的相对坐标和绝对坐标的转换、图片增强、将图片填充为正方形和将图片和boxes转换为tensor形式。
AUGMENTATION_TRANSFORMS = transforms.Compose([
AbsoluteLabels(),
DefaultAug(),
PadSquare(),
RelativeLabels(),
ToTensor(),
])
二、模型搭建
1.整体框架
在介绍models的具体实现方式之前,先来看一下YOLOv3的整体框架图(引用木盏的结构图):
网络的backbone采用了resnet的结构,输入图像为416x416尺寸,经过Darknet-53提取特征。output的尺寸分别为13x13x((5+classes)x3)、26x26x((5+classes)x3)、52x52x((5+classes)x3),分别对应经过32次、16次、8次下采样。其中,5+classes为对x、y、w、h、conf的预测加上class的数量。YOLOv3采用了anchors的方法,对目标的尺寸给予了一个先验指导,作者在数据集上对目标框的大小使用了k-means聚类,得到了9个不同尺寸的anchors,根据anchors的大小,每种尺寸的输出对应3个尺寸的anchors,这也就是为什么输出的第三个维度5+classes要乘以3,至于具体的实现方式将在下文结合代码进行介绍。
2.创建模块
在models.py中定义了create_modules函数用来创建各个模块,该函数会根据cfg文件中对各个层的参数定义生成对应的层加入module_list。
值得一提的是,cfg中的route层是concatenate级联操作层,对于级联和shortcut操作,代码中使用的是创建一个空操作层:
def create_modules(module_defs):
"""此处省略部分代码"""
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())
"""此处省略部分代码"""
class EmptyLayer(nn.Module):
"""Placeholder for 'route' and 'shortcut' layers"""
def __init__(self):
super(EmptyLayer, self).__init__()
相应的操作在后续Darknet类的forward中进行:
class Darknet(nn.Module):
"""此处省略部分代码"""
def forward(self, x, targets=None):
"""此处省略部分代码"""
elif module_def["type"] == "route":
x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)
elif module_def["type"] == "shortcut":
layer_i = int(module_def["from"])
x = layer_outputs[-1] + layer_outputs[layer_i]
"""此处省略部分代码"""
3.YOLO层
YOLO层应该是整个框架的精髓部分,网络的最终输出和loss的计算都将在这一层中完成。YOLO层初始化时需要三个ancho