YOLOv3.cfg文件解析
[net]
#Testing
#batch=1
#subdivisions=1
# 在测试的时候,设置 batch=1,subdivisions=1
#Training
batch=16
subdivisions=4
# 这里的 batch 与普遍意义上的 batch 不是一致的。
# 训练的过程中将一次性加载 16 张图片进内存,然后分 4 次完成前向传播,每次 4 张。
# 经过 16 张图片的前向传播以后,进行一次反向传播。
width=416
height=416
channels=3
# 设置图片进入网络的宽、高和通道个数。
# 由于 YOLOv3 的下采样一般是 32 倍,所以宽高必须能被 32 整除。
# 多尺度训练选择为 32 的倍数最小 320*320,最大 608*608。
# 长和宽越大,对小目标越好,但是占用显存也会高,需要权衡。
momentum=0.9
# 动量参数影响着梯度下降到最优值得速度。
decay=0.0005
# 权重衰减正则项,防止过拟合。
angle=0
# 数据增强,设置旋转角度。
saturation = 1.5
# 饱和度
exposure = 1.5
# 曝光量
hue=.1
# 色调
learning_rate=0.001
# 学习率: 刚开始训练时可以将学习率设置的高一点,而一定轮数之后,将其减小。
# 在训练过程中,一般根据训练轮数设置动态变化的学习率。
burn_in=1000
max_batches = 500200
# 最大 batch
policy=steps
# 学习率调整的策略,有以下 policy:constant, steps, exp, poly, step, sig, RANDOM,constant 等方式
#steps# 比较好理解,按照 steps 来改变学习率。
steps=400000,450000
scales=.1,.1
# 在达到 40000、45000 的时候将学习率乘以对应的 scale
--------------------- 卷积层 ----------------------
[convolutional]
batch_normalize=1
# 是否做 BN 操作
filters=32
# 输出特征图的数量
size=3
# 卷积核的尺寸
stride=1
# 做卷积运算的步长
pad=1
# 如果 pad 为 0,padding 由 padding 参数指定。
# 如果 pad 为 1,padding 大小为 size/2,padding 应该是对输入图像左边缘拓展的像素数量
activation=leaky
# 激活函数的类型:logistic,loggy,relu,elu,relie,plse,hardtan,lhtan,linear,ramp,leaky,tanh,stair
# alexeyAB 版添加了 mish, swish, nrom_chan 等新的激活函数
------------------------ 下采样 ------------------------
[convolutional]
batch_normalize=1
filters=128
size=3
stride=2
pad=1
activation=leaky
可以通过带入以上公式,可以得到 OutFeature 是 InFeature 的一半。
也可以使用 maxpooling 进行下采样:
[maxpool]
size=2
stride=2
------------------------ 上采样 -------------------------
[upsample]
stride=2
上采样是通过线性插值实现的。
----------------- Shortcut 和 Route 层 ------------------
[shortcut]
from=-3
activation=linear
#shortcut 操作是类似 ResNet 的跨层连接,参数 from 是 −3,
# 意思是 shortcut 的输出是当前层(记为0)与先前的倒数第三层(倒数-1,-2,-3)相加而得到。
# 通俗来讲就是 add 操作
[route]
layers = -1, 36
# 当属性有两个值,就是将上一层和第 36 层进行 concate
# 即沿深度的维度连接,这也要求 feature map 大小是一致的。
[route]
layers = -4
# 当属性只有一个值时,它会输出由该值索引的网络层的特征图。
# 本例子中就是提取从当前倒数第四个层输出
------------------------------ YOLO 层 ---------------------------
[convolutional]
size=1
stride=1
pad=1
filters=18
# 每一个 [region/yolo] 层前的最后一个卷积层中的
#filters=num(yolo 层个数)*(classes+5) ,5 的意义是 5 个坐标,
# 代表论文中的 tx,ty,tw,th,po
# 这里类别个数为 1,(1+5)*3=18
activation=linear
[yolo]
mask = 0,1,2
# 训练框 mask 的值是 0,1,2,
# 这意味着使用第一,第二和第三个 anchor
anchors = 10,13, 16,30, 33,23, 30,61, 62,45,59,119, 116,90, 156,198, 373,326
# 总共有三个检测层,共计 9 个 anchor
# 这里的 anchor 是由 kmeans 聚类算法得到的。
classes=1
# 类别个数
num=9
# 每个 grid 预测的 BoundingBox num/yolo 层个数
jitter=.3
# 利用数据抖动产生更多数据,
# 属于 TTA(Test Time Augmentation)
ignore_thresh = .5
# ignore_thresh 指得是参与计算的 IOU 阈值大小。
# 当预测的检测框与 ground true 的 IOU 大于 ignore_thresh 的时候,
# 不会参与 loss 的计算,否则,检测框将会参与损失计算。
# 目的是控制参与 loss 计算的检测框的规模,当 ignore_thresh 过于大,
# 接近于 1 的时候,那么参与检测框回归 loss 的个数就会比较少,同时也容易造成过拟合;
# 而如果 ignore_thresh 设置的过于小,那么参与计算的会数量规模就会很大。
# 同时也容易在进行检测框回归的时候造成欠拟合。
#ignore_thresh 一般选取 0.5-0.7 之间的一个值
# 小尺度(13*13)用的是 0.7,
# 大尺度(26*26)用的是 0.5。
数据组织、加载
参考:https://blog.csdn.net/DD_PP_JJ/article/details/104709299
在pytorch中,数据集加载主要是重构datasets类
,然后再使用dataloader中加载dataset,就构建好了数据部分。
下面是一个简单的使用模板(pytorch中的数据加载机制):
import os
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
# 根据自己的数据集格式进行重构
class MyDataset(Dataset):
def __init__(self):
#下载数据、初始化数据,都可以在这里完成
xy = np.loadtxt('label.txt', delimiter=',', dtype=np.float32)
# 使用numpy读取数据
self.x_data = torch.from_numpy(xy[:, 0:-1])
self.y_data = torch.from_numpy(xy[:, [-1]])
self.len = xy.shape[0]
def __getitem__(self, index):
# dataloader中使用该方法,通过index进行访问
return self.x_data[index], self.y_data[index]
def __len__(self):
# 查询数据集中数量,可以通过len(mydataset)得到
return self.len
# 实例化这个类,然后我们就得到了Dataset类型的数据,记下来就将这个类传给DataLoader,就可以了。
myDataset = MyDataset()
# 构建dataloader
train_loader = DataLoader(dataset=myDataset,
batch_size=32,
shuffle=True)
for epoch in range(2):
for i, data in enumerate(train_loader):
# 将数据从 train_loader 中读出来,一次读取的样本数是32个
inputs, labels = data
# 将这些数据转换成Variable类型
inputs, labels = Variable(inputs), Variable(labels)
# 模型训练...
建议阅读:
矩形训练相关:https://blog.csdn.net/songwsx/article/details/102639770
仿射变换:https://zhuanlan.zhihu.com/p/93822508
Rectangle Trainning:https://github.com/ultralytics/yolov3/issues/232
数据自由读取:https://zhuanlan.zhihu.com/p/30385675
超参数搜索与进化
参考:https://blog.csdn.net/DD_PP_JJ/article/details/104709330
YOLOv3代码中也提供了参数搜索,可以为对应的数据集进化一套合适的超参数。
在train.py,其中包含了一些数据增强参数设置:
# Hyperparameters
hyp = {'giou': 3.54, # giou loss gain
'cls': 37.4, # cls loss gain
'cls_pw': 1.0, # cls BCELoss positive_weight
'obj': 64.3, # obj loss gain (*=img_size/320 if img_size != 320)
'obj_pw': 1.0, # obj BCELoss positive_weight
'iou_t': 0.20, # iou training threshold
'lr0': 0.01, # initial learning rate (SGD=5E-3, Adam=5E-4)
'lrf': 0.0005, # final learning rate (with cos scheduler)
'momentum': 0.937, # SGD momentum
'weight_decay': 0.0005, # optimizer weight decay
'fl_gamma': 0.0, # focal loss gamma (efficientDet default is gamma=1.5)
'hsv_h': 0.0138, # image HSV-Hue augmentation (fraction)
'hsv_s': 0.678, # image HSV-Saturation augmentation (fraction)
'hsv_v': 0.36, # image HSV-Value augmentation (fraction)
'degrees': 1.98 * 0, # image rotation (+/- deg)
'translate': 0.05 * 0, # image translation (+/- fraction)
'scale': 0.05 * 0, # image scale (+/- gain)
'shear': 0.641 * 0} # image shear (+/- deg)
在训练的时候,train.py提供了一个可选参数--evolve
, 决定了是否进行超参数搜索与进化(默认是不开启超参数搜索的)。
python train.py --data data/voc.data --cfg cfg/yolov3-tiny.cfg --img-size 416 --epochs 273 --evolve
实际使用的时候,需要进行修改,train.py中的约444行:
for _ in range(1): # generations to evolve
将其中的1修改为你想设置的迭代数,比如200代,如果不设置,结果将会如下图所示,实际上就是只有一代。
网络模型的构建
参考:https://blog.csdn.net/DD_PP_JJ/article/details/104709403
1、如何从cfg文件构造模型。本文涉及到一个比较有用的部分就是bias的设置,可以提升mAP、F1、P、R等指标,还能让训练过程更加平滑。
2、修改网络结构很容易,只需要修改cfg文件即可。目前,cfg文件支持convolutional, maxpool, unsample, route, shortcut, yolo这几个层。
3、如果想要添加自定义的模块也很方便,比如说注意力机制模块、空洞卷积等,都可以简单地得到添加或者修改。
4、为了更加方便的理解cfg文件网络是如何构建的,在这里推荐一个Github上的网络结构可视化软件:Netron
train.py:
#Initialize model
model = Darknet(cfg, arc=opt.arc).to(device)
然后沿着Darknet实现进行讲解:
class Darknet(nn.Module):
# YOLOv3 object detection model
def __init__(self, cfg, img_size=(416, 416), arc='default'):
super(Darknet, self).__init__()
self.module_defs = parse_model_cfg(cfg)
self.module_list, self.routs = create_modules(self.module_defs, img_size, arc)
self.yolo_layers = get_yolo_layers(self)
# Darknet Header
self.version = np.array([0, 2, 5], dtype=np.int32)
# (int32) version info: major, minor, revision
self.seen = np.array([0], dtype=np.int64)
# (int64) number of images seen during training
比较关键的就是成员函变量module_defs、module_list、routs、yolo_layers
四个成员函数
YOLOLayer解析和推理过程
模型构建中最重要的YOLOLayer还没有梳理,本文将从代码的角度理解YOLOLayer的构建与实现
1、 Grid创建
需要注意的是这是针对某一层YOLOLayer,而不是所有的YOLOLayer
def create_grids(self,
img_size=416,
ng=(13, 13),
device='cpu',
type=torch.float32):
nx, ny = ng # 网格尺寸
self.img_size = max(img_size)
#下采样倍数为32
self.stride = self.img_size / max(ng)
# 划分网格,构建相对左上角的偏移量
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
# 通过以上例子很容易理解
self.grid_xy = torch.stack((xv, yv), 2).to(device).type(type).view(
(1, 1, ny, nx, 2))
# 处理anchor,将其除以下采样倍数
self.anchor_vec = self.anchors.to(device) / self.stride
self.anchor_wh = self.anchor_vec.view(1, self.na, 1, 1,
2).to(device).type(type)
self.ng = torch.Tensor(ng).to(device)
self.nx = nx
self.ny = ny
2、 YOLOLayer
训练过程:
YOLOLayer的作用就是对上一个卷积层得到的张量进行处理,具体可以看training过程涉及的代码(暂时不关心ONNX部分的代码):
class YOLOLayer(nn.Module):
def __init__(self, anchors, nc, img_size, yolo_index, arc):
super(YOLOLayer, self).__init__()
self.anchors = torch.Tensor(anchors)
self.na = len(anchors) # 该YOLOLayer分配给每个grid的anchor的个数
self.nc = nc # 类别个数
self.no = nc + 5 # 每个格子对应输出的维度 class + 5 中5代表x,y,w,h,conf
self.nx = 0 # 初始化x方向上的格子数量
self.ny = 0 # 初始化y方向上的格子数量
self.arc = arc
if ONNX_EXPORT: # grids must be computed in __init__
stride = [32, 16, 8][yolo_index] # stride of this layer
nx = int(img_size[1] / stride) # number x grid points
ny = int(img_size[0] / stride) # number y grid points
create_grids(self, img_size, (nx, ny))
def forward(self, p, img_size, var=None):
'''
onnx代表开放式神经网络交换
pytorch中的模型都可以导出或转换为标准ONNX格式
在模型采用ONNX格式后,即可在各种平台和设备上运行
在这里ONNX代表规范化的推理过程
'''
if ONNX_EXPORT:
bs = 1 # batch size
else:
bs, _, ny, nx = p.shape # bs, 255, 13, 13
if (self.nx, self.ny) != (nx, ny):
create_grids(self, img_size, (nx, ny), p.device, p.dtype)
# p.view(bs, 255, 13, 13) -- > (bs, 3, 13, 13, 85)
# (bs, anchors, grid, grid, classes + xywh)
p = p.view(bs, self.na, self.no, self.ny,
self.nx).permute(0, 1, 3, 4, 2).contiguous()
if self.training:
return p
在理解以上代码的时候,需要理解每一个通道所代表的意义,原先的P是由上一层卷积得到的feature map, 形状为(以80个类别、输入416、下采样32倍为例):【batch size, anchor×(80+5), 13, 13】,在训练的过程中,将feature map通过张量操作转化的形状为:【batch size, anchor, 13, 13, 85】。
测试过程:
# p的形状目前为:【bs, anchor_num, gridx,gridy,xywhc+class】
else: # 测试推理过程
# s = 1.5 # scale_xy (pxy = pxy * s - (s - 1) / 2)
io = p.clone() # 测试过程输出就是io
io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid_xy # xy
# grid_xy是左上角再加上偏移量io[...:2]代表xy偏移
io[..., 2:4] = torch.exp(
io[..., 2:4]) * self.anchor_wh # wh yolo method
# io[..., 2:4] = ((torch.sigmoid(io[..., 2:4]) * 2) ** 3) * self.anchor_wh
# wh power method
io[..., :4] *= self.stride
if 'default' in self.arc: # seperate obj and cls
torch.sigmoid_(io[..., 4])
elif 'BCE' in self.arc: # unified BCE (80 classes)
torch.sigmoid_(io[..., 5:])
io[..., 4] = 1
elif 'CE' in self.arc: # unified CE (1 background + 80 classes)
io[..., 4:] = F.softmax(io[..., 4:], dim=4)
io[..., 4] = 1
if self.nc == 1:
io[..., 5] = 1
# single-class model https://github.com/ultralytics/yolov3/issues/235
# reshape from [1, 3, 13, 13, 85] to [1, 507, 85]
return io.view(bs, -1, self.no), p
理解以上内容是需要对应以下公式:
io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid_xy # xy
# grid_xy是左上角再加上偏移量io[...:2]代表xy偏移
# wh yolo method
io[..., 2:4] = torch.exp(io[..., 2:4]) * self.anchor_wh
class部分:
在类别部分,提供了几种方法,根据arc参数来进行不同模式的选择。以CE(crossEntropy)为例:
#io: (bs, anchors, grid, grid, xywh+classes)
io[..., 4:] = F.softmax(io[..., 4:], dim=4)# 使用softmax
io[..., 4] = 1
Loss部分计算
YOLOv1是一个anchor-free的,从YOLOv2开始引入了Anchor,在VOC2007数据集上将mAP提升了10个百分点。YOLOv3也继续使用了Anchor,本文主要讲ultralytics版YOLOv3的Loss部分的计算, 实际上这部分loss和原版差距非常大,并且可以通过arc指定loss的构建方式, 如果想看原版的loss可以在release的v6中下载源码。