最新的YOLOv5增加了wandb功能,要先注册一个wandb的账号
将申请的API配置到电脑上,即可实时看到损失、权重和偏执。
YOLOv5的配置文件是.yaml格式
以yolov5l.yaml为例
# Parameters
nc: 80 # number of classes
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
nc:代表分类类别的数量
depth_multiple:模型深度,即控制模块的数量,模块的数量=depth_multipledepth;
width_multiple:模型宽度,控制的是卷积核的数量。卷积核的数量=numwidth;
根据实际项目需求去调整,难以学习的或者数据规模较大的,用大一些的宽度和深度超参。小一些的就调小些。
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
anchors:包含的是不同尺度的特征图中anchors的尺寸,10,13是一组,16,30是一组…每个尺度的特征图有三种尺寸的anchor。YOLOv5的anchor设计思想是在大特征图上检测小目标,在小特征图上检测大目标。P3/8…等等代表了输入的特征图经下采样后与原特征图的比例。
# YOLOv5 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Focus, [64, 3]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 9, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 1, SPP, [1024, [5, 9, 13]]],
[-1, 3, C3, [1024, False]], # 9
]
YOLOv5的backbone中四个参数:
**from:**这一层的input从哪里来,-1代表从上一层
number:代表这一层的卷积核数量
model:这一层使用到的模块
args:这一层
:对feature map进backbone前进行切片操作,具体操作就是每隔一个像素值取一个值,这样就拿到了四张图片,且没有信息丢失。
640×640×3的图像,经过Focus先切片,再通过concat从深度上连接四个切片后的feature map,得到320×320×12的feature map,再用卷积增加通道数至128,就得到了320×320×128的feature map。
Focus作用:和普通下采样比,保证信息不丢失。提高FLOPs速度
class Focus(nn.Module):
# Focus wh information into c-space
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Focus, self).__init__()
self.conv = Conv(c1 * 4, c2, k, s, p, g, act)
# self.contract = Contract(gain=2)
def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2)
return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
# return self.conv(self.contract(x))
class Conv(nn.module):
# standard convolution
# 这个类做了三件事 架构图中的标准卷积: conv+BN+hardswish
# init初始化构造函数
def __init__(self,c1,c2,k=1,s=1,p=None,g=1,act=True): # ch_in ch_out kernel, stride, padding, groups
super(Conv, self).__init__()
'''
nn.conv2d函数的基本参数是:
nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, paddinng_mode='zeros')
参数:nn.Conv 参考链接https://blog.csdn.net/qq_26369907/article/details/88366147
in_channel:输入数据的通道数,例RGB图片的通道数为3
out_channel:输出数据的通道数,这个根据模型调整
kennel_size:卷积核大小,可以是int,或tuple,kennel_size=2,意味着卷积大小(2,2), kennel_size=(2,3),意味着卷积大小(2,3),即非正方形卷积
stride: 步长,默认为1, 与kennel_size类似, stride=2, 意味着步长上下左右扫描皆为2, stride=(2,3),左右三秒步长为2,上下为3
padding: 零填充
groups: 从输入通道到输出通道阻塞连接数,通道分组的参数,输入通道数,输出通道数必须同时满足被groups整除
groups:如果输出通道为6,输入通道也为6,假设groups为3,卷积核为1*1,;则卷积核的shape为2*1*1,即把输入通道分成了3份;那么卷积核的个数呢?之前是由输出通道
决定的,这里也一样,输出通道为6,那么就有6个卷积核。这里实际是将卷积核也平分为groups份,在groups份特征图上计算,以输入输出都为6为例,每个2*h*w的特征图子层
就有且仅有2个卷积核,最后相加恰好是6,这里可以起到的作用是不同通道分别计算特征
bias:如果为True,则向输出添加可学习的偏置
'''
# conv调用nn.Conv2d函数, p采用autopad, 不使用偏置bias=False 因为下面做融合时,这个卷积的bias会被消掉, 所以不用
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k,p), groups=g, bias=False)
self.bn = nn.Batch2dNorm2d(c2)
# 如果act是true则使用nn.SiLU 否则不使用激活函数
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x): # 前向计算,网络执行的顺序是根据forward函数来决定的
return self.act(self.bn(self.conv(x))) # 先conv卷积然后在bn最后act激活
def fuseforward(self,x): # 前向融合计算
return self.act(self.conv(x)) # 这里只有卷积和激活
标准Conv层包含一层卷积、一层BN和一层leakyrelu(CBL)
class Bottleneck(nn.Module):
# Standard bottleneck
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion
super(Bottleneck, self).__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_, c2, 3, 1, g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
paras:c1:bottleneck的输入维度
c2:bottleneck的输出维度
shortcut:是否加入shortcut连接
g:通道分组数,c1、c2必须都能整除group
e:expand_ratio,使用0.5即变为输入的1/2
SPP模块用来解决输入图像尺寸不统一的问题,不同大小特征的融合。有利于待检测图像中目标大小差异较大的情况。
2.数据读取与增强
YOLOv5的数据读取是在utils下面的datasets.py里完成的
(1)读取的参数设置
都是在LoadImagesAndLabels类下说明
def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, rect=False, image_weights=False,
cache_images=False, single_cls=False, stride=32, pad=0.0, prefix=''):
self.img_size = img_size
self.augment = augment
self.hyp = hyp
self.image_weights = image_weights
self.rect = False if image_weights else rect
self.mosaic = self.augment and not self.rect # load 4 images at a time into a mosaic (only during training)
self.mosaic_border = [-img_size // 2, -img_size // 2]
self.stride = stride
self.path = path
self.albumentations = Albumentations() if augment else None
能指定img_size和batchsize以及augment,调参的时候调这些就够了
(2)读取数据集路径下的文件
用了一个异常处理机制来读取
try:
f = [] # image files
for p in path if isinstance(path, list) else [path]:
p = Path(p) # os-agnostic
if p.is_dir(): # dir
f += glob.glob(str(p / '**' / '*.*'), recursive=True)
# f = list(p.rglob('**/*.*')) # pathlib
elif p.is_file(): # file
with open(p, 'r') as t:
t = t.read().strip().splitlines()
parent = str(p.parent) + os.sep
f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path
# f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib)
else:
raise Exception(f'{prefix}{p} does not exist')
self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS])
# self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib
assert self.img_files, f'{prefix}No images found'
except Exception as e:
raise Exception(f'{prefix}Error loading data from {path}: {e}\nSee {HELP_URL}')
其中os.path.isfile判断某一对象(需提供绝对路径)是否为文件
os.path.isdir()判断某一对象(需提供绝对路径)是否为目录
os.listdir():返回一个列表,其中包含有指定路径下的目录和文件的名称
获取数据集路径的两个判断说明有两种读取数据集的方式,一种是把图片路径放到txt文件中去别的地方读,第二种就是直接将folder放到path下