0


YOLOv5 最详细的源码逐行解读(二: 网络结构)

1. Yolov5s的网络结构

1.1 yaml文件解读

Yolov5中,网络模型的配置放在yaml文件中,而yolov5s放置在models/yolov5s.yaml文件中

参考视频:
bilibili
yolov5s.yaml文件内容如下:

# YOLOv5 🚀 by Ultralytics, GPL-3.0 license# Parametersnc: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 backbonebackbone:# [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 headhead:[[-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库解析为字典对象

1.2 yolov5s网络结构解读

对应的结构图如下:
在这里插入图片描述
图片来源: 文首视频链接

网络中使用了特征融合, 第4、6、9层的特征均被连接到Concat层进行检测,特征融合的意义在于第4、6、9层的抽象度不同,第四层更容易检测小目标,第9层更容易检测大目标。

2. yolo.py 解读

文件地址:
这一部分具体解读是如何构建网络模块的

2.1 class Model 92~250行

2.1.1 init 94~130行

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__()ifisinstance(cfg,dict):# 如果cfg是字典
            self.yaml = cfg 
        else:# is *.yamlimport yaml  # 加载yaml模块
            self.yaml_file = Path(cfg).name
            withopen(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赋给内部变量chif 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 valueif 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 inrange(self.yaml['nc'])]# 初始化类名列表,默认为[0,1,2...]
        self.inplace = self.yaml.get('inplace',True)#确定步长、步长对应的锚框
        m = self.model[-1]# Detect()ifisinstance(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('')

2.2 parse_model方法 243~295行

defparse_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)ifisinstance(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层输出通道数为nfor i,(f, n, m, args)inenumerate(d['backbone']+ d['head']):# f, n, m, args分别对应from, number, module, args
        m =eval(m)ifisinstance(m,str)else m  # 根据字符串m的内容创建类for j, a inenumerate(args):# args是一个列表,这一步把列表中的内容取出来try:
                args[j]=eval(a)ifisinstance(a,str)else a  # eval stringsexcept NameError:pass

        n = n_ =max(round(n * gd),1)if n >1else 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 =1elif 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])ifisinstance(args[1],int):# number of anchors
                args[1]=[list(range(args[1]*2))]*len(f)elif m is Contract:
            c2 = ch[f]* args[0]**2elif m is Expand:
            c2 = ch[f]// args[0]**2else:
            c2 = ch[f]

        m_ = nn.Sequential(*(m(*args)for _ inrange(n)))if n >1else 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]ifisinstance(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

本文转载自: https://blog.csdn.net/weixin_46183779/article/details/125832454
版权归原作者 supermax2020 所有, 如有侵权,请联系我们删除。

“YOLOv5 最详细的源码逐行解读(二: 网络结构)”的评论:

还没有评论