Yolov5中,网络模型的配置放在yaml文件中,而yolov5s放置在models/yolov5s.yaml文件中
参考视频:
bilibili
yolov5s.yaml文件内容如下:
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 80 # 检测类比的数量
depth_multiple: 0.33 # 模型层数因子
width_multiple: 0.50 # 模型通道数因子
# 如何理解这个depth_multiple和width_multiple呢?它决定的是整个模型中的深度(层数)和宽度(通道数)
# 假如某一层参数中深度设置为3,则说明他实际有3×depth_multiple层,假如某一层参数中的通道数是64,他实际的通道数是64×width_multiple
# 为什么设置这个量呢?这是为了便于全局调整网络大小,比较一下yolov5m.yaml,yolov5x.yaml,你会发现除了这两个因子之外网络结构完全一样。
anchors: # 9个anchor,其中P表示特征图的层级,P3/8该层特征图缩放为1/8,是第3层特征
- [10,13, 16,30, 33,23] # P3/8, 表示[10,13],[16,30], [33,23]3个anchor
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [64, 6, 2, 2]], # 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, 6, 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, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
# YOLOv5 v6.0 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
其中一层网络的参数是用列表实现的,比如:
[-1, 1, Conv, [64, 6, 2, 2]]
四个参数的含义分别是:
-1: 输入来自上一层,如果是正数i则代表第i层
1:使用一个网络模块
Conv: 该层的网络层名字是Conv
[64, 6, 2, 2]: Conv层的四个参数
yaml文件可以被yaml库解析为字典对象
对应的结构图如下:
图片来源: 文首视频链接
网络中使用了特征融合, 第4、6、9层的特征均被连接到Concat层进行检测,特征融合的意义在于第4、6、9层的抽象度不同,第四层更容易检测小目标,第9层更容易检测大目标。
文件地址:
这一部分具体解读是如何构建网络模块的
def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
# cfg: 可以是字典,也可以是yaml文件路径
# ch:输入通道数
# nc:类的个数
# anchors:所有的anchor列表
super().__init__()
if isinstance(cfg, dict):# 如果cfg是字典
self.yaml = cfg
else: # is *.yaml
import yaml # 加载yaml模块
self.yaml_file = Path(cfg).name
with open(cfg, encoding='ascii', errors='ignore') as f:
self.yaml = yaml.safe_load(f) # 从yaml文件中加载出字典
# Define model
ch = self.yaml['ch'] = self.yaml.get('ch', ch)
# ch: 输入通道数。 假如self.yaml有键‘ch’,则将该键对应的值赋给内部变量ch。假如没有‘ch’,则将形参ch赋给内部变量ch
if nc and nc != self.yaml['nc']:
# 假如yaml中的nc和方法形参中的nc不一致,则覆盖yaml中的nc。
LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value
if anchors:
# 假如yaml中的anchors和方法形参中的anchors不一致,则覆盖yaml中的anchors。
LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
self.yaml['anchors'] = round(anchors) # override yaml value
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # 得到模型,以及对应的特征图保存标签。
# 所谓的特征图保存标签是一个列表,它的内容可能是[1,5],表示第1、5层前向传播后的特征图需要保存下来,以便跳跃连接使用
# 详细解读见2.2
self.names = [str(i) for i in range(self.yaml['nc'])] # 初始化类名列表,默认为[0,1,2...]
self.inplace = self.yaml.get('inplace', True)
#确定步长、步长对应的锚框
m = self.model[-1] # Detect()
if isinstance(m, Detect):# 如果模型的最后一层是detect模块
s = 256 # 2x min stride
m.inplace = self.inplace
m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # 使用一张空白图片作为模型的输入,并以此得到实际步长。(默认的设置中,步长是8,16,32)
check_anchor_order(m) # anchor的顺序应该是从小到大,这里排一下序
m.anchors /= m.stride.view(-1, 1, 1)
# 得到anchor在实际的特征图中的位置
# 因为加载的原始anchor大小是相对于原图的像素,但是经过卷积池化之后,图片也就缩放了
# 对于的anchor也需要缩放操作
self.stride = m.stride
self._initialize_biases() # only run once
# Init weights, biases
initialize_weights(self)
self.info()
LOGGER.info('')
def parse_model(d, ch): # model_dict, input_channels(3)
LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}")# 打印信息,
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']# 加载字典中的anchors、nc、depth_multiple、width_multiple。
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors
# na:anchor的数量
#anchors的形式见“1.1 yaml文件解读”,以yolov5s.yaml中定义的为例,
#anchors[0]是第一行的一个anchors,它的长度是6,表示3个w-h对,即3个anchor,这里的na也应当为3
no = na * (nc + 5)
# 每一个anchor输出的数据数量 = anchors * (classes + 5)
# 其中5代表x,y,w,h,conf五个量
layers, save, c2 = [], [], ch[-1]
# layers: 所有的网络层
# save: 标记该层网络的特征图是否需要保存(因为模型中存在跳跃连接,有的特征图之后需要用到)比如save=[1,2,5]则第1,2,5层需要保存特征图
# ch 该层所输出的通道数,比如save[i]=n表示第i层输出通道数为n
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):
# f, n, m, args分别对应from, number, module, args
m = eval(m) if isinstance(m, str) else m # 根据字符串m的内容创建类
for j, a in enumerate(args):
# args是一个列表,这一步把列表中的内容取出来
try:
args[j] = eval(a) if isinstance(a, str) else a # eval strings
except NameError:
pass
n = n_ = max(round(n * gd), 1) if n > 1 else n
# 将深度与深度因子相乘,计算层深度。深度最小为1.
if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
BottleneckCSP, C3, C3TR, C3SPP, C3Ghost]:
# 假如module名字正确,则开始加载
c1, c2 = ch[f], args[0]
# c1: 输入通道数 c2:输出通道数
if c2 != no: # 该层不是最后一层,则将通道数乘以宽度因子
c2 = make_divisible(c2 * gw, 8)
# 也就是说,宽度因子作用于除了最后一层之外的所有层
args = [c1, c2, *args[1:]]
# 将前面的运算结果保存在args中,它也就是最终的方法参数。
# 上面这几步主要处理了一层网络的参数
if m in [BottleneckCSP, C3, C3TR, C3Ghost]: # 根据每层网络参数的不同,分别处理参数
#具体各个类的参数是什么请参考它们的__init__方法,这里不再详细解释了
args.insert(2, n)
n = 1
elif m is nn.BatchNorm2d:
args = [ch[f]]
elif m is Concat:
c2 = sum(ch[x] for x in f)
elif m is Detect:
args.append([ch[x] for x in f])
if isinstance(args[1], int): # number of anchors
args[1] = [list(range(args[1] * 2))] * len(f)
elif m is Contract:
c2 = ch[f] * args[0] ** 2
elif m is Expand:
c2 = ch[f] // args[0] ** 2
else:
c2 = ch[f]
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
# 构建整个网络模块
# 如果n>1,就需要加入n层网络。
t = str(m)[8:-2].replace('__main__.', '') # t是类名
np = sum(x.numel() for x in m_.parameters()) # np: 参数个数
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}') # print
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)
#如果x不是-1,则将其保存在save列表中,表示该层需要保存特征图
layers.append(m_)# 将新创建的layer添加到layers数组中
if i == 0: # 如果是初次迭代,则新创建一个ch(因为形参ch在创建第一个网络模块时需要用到,所以创建网络模块之后再初始化ch)
ch = []
ch.append(c2)
return nn.Sequential(*layers), sorted(save)# 将所有的层封装为nn.Sequential