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


    1. Yolov5s的网络结构

    1.1 yaml文件解读

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    其中一层网络的参数是用列表实现的,比如:
    [-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__()
            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('')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    2.2 parse_model方法 243~295行

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
  • 相关阅读:
    nacos 适配瀚高数据库、ARM 架构
    百度智能云正式上线Python SDK版本并全面开源!
    PostgreSQL数据库统计信息——analyze大致流程
    HDU-3549 Flow Problem 简单最大流
    【从零开始学习 SystemVerilog】11.3、SystemVerilog 断言—— Concurrent Assertions(并发断言)
    Nginx:防盗链原理和配置
    2023年【陕西省安全员C证】考试内容及陕西省安全员C证最新解析
    【牛客-剑指offer-数据结构篇】【图解】JZ76 删除链表中重复的结点 两种思路 Java实现
    docker-compose概述与简单编排部署
    基于图神经网络的图像分类,遥感图像分析
  • 原文地址:https://blog.csdn.net/weixin_46183779/article/details/125832454