yolov8逐步分解(1)--默认参数&超参配置文件加载

yolov8逐步分解(2)_DetectionTrainer类初始化过程

yolov8逐步分解(3)_trainer训练之模型加载

        接上篇模型加载文章,本节将详细介绍yolov8检测模型DetectionModel()的实例化过程及模型的解析构造过程。

1. DetectionModel()初始化

class DetectionModel(BaseModel):
    """YOLOv8 detection model."""

    def __init__(self, cfg='yolov8n.yaml', ch=3, nc=None, verbose=True):  # model, input channels, number of classes
        super().__init__()
        self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg)  # cfg dict #本次输入的是字典
        # Define model
        ch = self.yaml['ch'] = self.yaml.get('ch', ch)  # input channels 3
        if nc and nc != self.yaml['nc']: #coco128数据集 nc=80
            LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
            self.yaml['nc'] = nc  # override yaml value
        self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose)  # model, savelist
        self.names = {i: f'{i}' for i in range(self.yaml['nc'])}  # default names dict
        self.inplace = self.yaml.get('inplace', True)
        # Build strides
        m = self.model[-1]  # Detect()
        if isinstance(m, (Detect, Segment, Pose)):
            s = 256  # 2x min stride 设置默认步长
            m.inplace = self.inplace
            #如果是Segment, Pose,返回前向推理的第一个值,否则返回所有值
            forward = lambda x: self.forward(x)[0] if isinstance(m, (Segment, Pose)) else self.forward(x)
            #计算步长 m.stride,通过对一个全零的输入张量进行前向传播,然后计算步长与输入张量维度之比的值
            m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))])  # forward #值为tensor([8, 16, 32])
            self.stride = m.stride
            m.bias_init()  # only run once
        else:
            self.stride = torch.Tensor([32])  # default stride for i.e. RTDETR
        # Init weights, biases
        initialize_weights(self)
        if verbose:
            self.info()
            LOGGER.info('')

上面代码定义了一个使用YOLOv8架构的检测模型。以下是代码的解释:

(1)DetectionModel类定义了一个检测模型,使用了YOLOv8架构。它继承自BaseModel类。

(2)__init__方法是DetectionModel类的构造函数。它接受几个参数:

                cfg:模型的YAML配置文件路径(默认为'yolov8n.yaml')。

                ch:输入通道数(默认为3)。

                nc:类别数量(默认为None)。

                verbose:是否打印详细信息(默认为True)。

(3)在构造函数中,首先根据提供的cfg参数加载YAML配置文件,并将其存储在self.yaml属性中。

(4)接下来,根据配置文件和输入通道数构建模型。如果提供了nc参数且与配置文件中的类别数量不同,会覆盖配置文件中的类别数量。

(5)self.model存储了构建的模型,self.save是一个保存模型的列表。

(6)self.names是一个字典,存储了类别的名称,默认情况下使用类别的索引作为名称。

(7)self.inplace是一个布尔值,指定是否进行原地操作(in-place)。

(8)构建步长(strides):      

        a.获取模型中的最后一个模块 m = self.model[-1]。

        b.如果模块是Detect、Segment或Pose类型的实例,则设置默认步长为256。

        c.根据模型的类型,通过前向传播计算步长m.stride。

        d.m.bias_init()用于初始化模块的偏置项(bias),仅运行一次。

(9) 如果模型不是Detect、Segment或Pose类型的实例,设置默认步长为32。

(10) 调用initialize_weights函数来初始化模型的权重和偏置项。

(11) 如果verbose为True,则打印模型的信息和空行。

2. 模型解析

        parse_model函数的目的是将一个包含模型架构信息的字典解析为一个PyTorch模型。它根据字典中的参数和层信息构建模型,并返回该模型及需要保存的层的索引.

def parse_model(d, ch, verbose=True):  # model_dict, input_channels(3)
    # Parse a YOLO model.yaml dictionary into a PyTorch model
    import ast  #用于字符串转换为 Python 对象

    # Args
    '''
    根据字典 d 解析一些参数,包括 nc(类别数)、act(激活函数)、scales(缩放参数)、
    depth_multiple(深度倍数)、width_multiple(宽度倍数)和 kpt_shape(关键点形状)。
    其中,scales 是一个字典,记录了不同缩放尺度下的深度、宽度和最大通道数。
    '''
    max_channels = float('inf')
    nc, act, scales = (d.get(x) for x in ('nc', 'activation', 'scales'))  #
    depth, width, kpt_shape = (d.get(x, 1.0) for x in ('depth_multiple', 'width_multiple', 'kpt_shape'))
    if scales:
        scale = d.get('scale')
        if not scale:
            scale = tuple(scales.keys())[0] #如果未提供缩放尺度,则默认使用 scales 的第一个尺度
            LOGGER.warning(f"WARNING ⚠️ no model scale passed. Assuming scale='{scale}'.")
        depth, width, max_channels = scales[scale]

    '''
    函数检查是否存在激活函数 act,如果存在,则重新定义默认的激活函数为 act 对应的激活函数(通过 eval 函数实现)
    '''
    if act:
        Conv.default_act = eval(act)  # redefine default activation, i.e. Conv.default_act = nn.SiLU()
        if verbose:
            LOGGER.info(f"{colorstr('activation:')} {act}")  # print

    if verbose:
        LOGGER.info(f"\n{'':>3}{'from':>20}{'n':>3}{'params':>10}  {'module':<45}{'arguments':<30}")
    
    '''
    输入通道数 ch 转换为一个列表。
    layers:用于保存模型的层信息
    save:用于保存需要保存的层的索引
    c2:表示当前的输出通道数,初始化为 ch 列表的最后一个元素(即输入通道数)
    '''
    ch = [ch] 
    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
     
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, args
        m = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m]  # get module
        for j, a in enumerate(args):
            if isinstance(a, str):
                with contextlib.suppress(ValueError):
                    args[j] = locals()[a] if a in locals() else ast.literal_eval(a)

        n = n_ = max(round(n * depth), 1) if n > 1 else n  # depth gain
        if m in (Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3):
            c1, c2 = ch[f], args[0]
            if c2 != nc:  # if c2 not equal to number of classes (i.e. for Classify() output)
                c2 = make_divisible(min(c2, max_channels) * width, 8)
            args = [c1, c2, *args[1:]]
            if m in (BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, C3x, RepC3):
                args.insert(2, n)  # number of repeats
                n = 1
        elif m is AIFI:
            args = [ch[f], *args]
        elif m in (HGStem, HGBlock):
            c1, cm, c2 = ch[f], args[0], args[1]
            args = [c1, cm, c2, *args[2:]]
            if m is HGBlock:
                args.insert(4, n)  # number of repeats
                n = 1
        elif m is nn.BatchNorm2d:
            args = [ch[f]]
        elif m is Concat:
            c2 = sum(ch[x] for x in f)
        elif m in (Detect, Segment, Pose, RTDETRDecoder):
            args.append([ch[x] for x in f])
            if m is Segment:
                args[2] = make_divisible(min(args[2], max_channels) * width, 8)
        else:
            c2 = ch[f]
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        t = str(m)[8:-2].replace('__main__.', '')  # module type
        m.np = sum(x.numel() for x in m_.parameters())  # number params
        m_.i, m_.f, m_.type = i, f, t  # attach index, 'from' index, type
        if verbose:
            LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m.np:10.0f}  {t:<45}{str(args):<30}')  # print
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        layers.append(m_)
        if i == 0:
            ch = []
        ch.append(c2)
    return nn.Sequential(*layers), sorted(save)

这段代码用于解析一个YOLO模型的模型字典,并将其转换为PyTorch模型。以下是代码的解释:

(1) parse_model函数接受三个参数:d是模型的字典表示,ch是输入通道数,verbose表示是否打印详细信息。

(2) 代码中导入了ast模块,用于将字符串转换为Python对象。

(3) 最大通道数(max_channels):max_channels用于限制模型中的通道数。默认情况下,它被设置为无穷大,即没有通道数的限制。然而,如果在模型字典中提供了scales参数,max_channels将根据缩放尺度进行设置。缩放尺度由scales字典提供,其中记录了不同缩放尺度下的深度、宽度和最大通道数。在选择缩放尺度后,depth、width和max_channels将被相应地设置为该缩放尺度下的值。

(4) 首先从模型字典中提取一些参数,包括类别数(nc)、激活函数(act)和缩放参数(scales)。depth_multiple(深度倍数)、width_multiple(宽度倍数)和 kpt_shape(关键点形状)

(5) 如果存在缩放参数(scales),则根据提供的缩放尺度(scale)获取对应的深度、宽度和最大通道数。

(6) 默认激活函数:如果字典中指定了激活函数act,则将默认的激活函数Conv.default_act重新定义为与act对应的激活函数。这是通过使用eval函数将字符串转换为相应的Python对象来实现的。

(7) 打印信息(可选):如果verbose参数为True,则函数会打印详细的模型信息。这些信息包括模块的索引、来源、数量、参数数量以及模块类型和参数列表等。

(8) 初始化一些变量,包括输入通道数列表(ch),保存模型层的列表(layers)和需要保存的层的索引列表(save)。

(9) 构建模型层:根据字典中的backbone和head部分的信息,函数逐层构建模型。根据模块名称,函数动态地获取对应的模块构造函数,并根据提供的参数进行实例化。如果某些参数是字符串类型,则尝试将其转换为相应的Python对象。在构建过程中,还会应用一些特定的操作,例如计算深度倍数、调整通道数等。

        a. 遍历模型字典中的每个模块,包括backbone和head中的模块。

        b. 根据模块的类型和参数构建相应的PyTorch模块,并将其添加到layers列表中。

        c. 如果模块是Classify、Conv、ConvTranspose等类型的实例,调整输入通道数和输出通道数。

        d. 如果模块是Concat类型的实例,计算输入通道数。

        e. 如果模块是Detect、Segment、Pose或RTDETRDecoder类型的实例,将对应模块的输入通道数添加到参数列表中。

        f. 如果模块是其他类型的实例,更新当前的输出通道数。

2.1 分步讲解

if m in (Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3):
            c1, c2 = ch[f], args[0]
            if c2 != nc:  # if c2 not equal to number of classes (i.e. for Classify() output)
                c2 = make_divisible(min(c2, max_channels) * width, 8)
            args = [c1, c2, *args[1:]]
            if m in (BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, C3x, RepC3):
                args.insert(2, n)  # number of repeats
                n = 1

        检查变量m是否在给定的模块类型元组中。这些模块类型包括Classify、Conv、ConvTranspose、GhostConv、Bottleneck、GhostBottleneck、SPP、SPPF、DWConv、Focus、BottleneckCSP、C1、C2、C2f、C3、C3TR、C3Ghost、nn.ConvTranspose2d、DWConvTranspose2d、C3x和RepC3。如果m属于其中任何一种类型,代码将执行以下操作:

        提取通道数:根据变量ch和args中的值,获取输入通道数c1和输出通道数c2。通道数是构建模块所需的重要参数。

        检查输出通道数:如果c2不等于类别数nc(例如用于Classify模块的输出),则将c2设置为最大通道数max_channels与width参数的乘积,同时确保结果是8的倍数。这里使用了make_divisible函数来计算最接近的8的倍数。

        更新参数列表:将c1和c2插入到args列表的前两个位置,以确保参数的正确顺序。如果模块类型属于BottleneckCSP、C1、C2、C2f、C3、C3TR、C3Ghost、C3x或RepC3,还会将重复次数n插入到参数列表的第三个位置。

        elif m is AIFI:
            args = [ch[f], *args]
        elif m in (HGStem, HGBlock):
            c1, cm, c2 = ch[f], args[0], args[1]
            args = [c1, cm, c2, *args[2:]]
            if m is HGBlock:
                args.insert(4, n)  # number of repeats
                n = 1

检查变量m是否等于AIFI。如果是,代码将执行以下操作。

        更新参数列表:将输入通道数ch[f]插入到args列表的第一个位置,以确保参数的正确顺序。

接下来,代码检查m是否属于模块类型元组(HGStem, HGBlock)中的任何一种。如果是,代码将执行以下操作:

        提取通道数:根据变量ch和args中的值,获取输入通道数c1、中间通道数cm和输出通道数c2。这些通道数是构建模块所需的重要参数。

        更新参数列表:将c1、cm和c2插入到args列表的前三个位置,以确保参数的正确顺序。如果模块类型是HGBlock,还会将重复次数n插入到参数列表的第五个位置。

        elif m is nn.BatchNorm2d:
            args = [ch[f]]
        elif m is Concat:
            c2 = sum(ch[x] for x in f)
        elif m in (Detect, Segment, Pose, RTDETRDecoder):
            args.append([ch[x] for x in f])
            if m is Segment:
                args[2] = make_divisible(min(args[2], max_channels) * width, 8)
        else:
            c2 = ch[f]

a. 检查变量m是否等于nn.BatchNorm2d。如果是,将执行以下操作:

        更新参数列表:将输入通道数ch[f]插入到args列表的第一个位置,以确保参数的正确顺序。

b. 检查m是否等于Concat。如果是将执行以下操作:

        计算输出通道数:根据变量ch和f中的值,计算需要将其连接的模块的输出通道数。将所有f中的模块的通道数相加,并将结果赋给c2。

c. 检查m是否属于模块类型元组(Detect, Segment, Pose, RTDETRDecoder)中的任何一种。如果是将执行以下操作:

        更新参数列表:将f中每个模块的通道数ch[x]以列表的形式附加到args列表中。

        如果模块类型是Segment,则执行以下操作:

        计算输出通道数:将args列表中的第三个参数与最大通道数max_channels相乘,然后乘以width,并确保结果是8的倍数。这里使用了make_divisible函数来计算最接近的8的倍数。

d.如果以上条件都不满足,代码将执行以下操作。

        提取通道数:将输入通道数ch[f]赋值给c2。

        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        t = str(m)[8:-2].replace('__main__.', '')  # module type
        m.np = sum(x.numel() for x in m_.parameters())  # number params
        m_.i, m_.f, m_.type = i, f, t  # attach index, 'from' index, type
        if verbose:
            LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m.np:10.0f}  {t:<45}{str(args):<30}')  # print
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        layers.append(m_)
        if i == 0:
            ch = []
        ch.append(c2)
    return nn.Sequential(*layers), sorted(save)

这段代码是parse_model函数中的最后一部分,用于构建模块并返回最终的模型。

        a. 代码根据重复次数n的值决定是使用nn.Sequential包装模块还是直接使用模块。如果n大于1,代码将使用nn.Sequential将模块重复n次;否则,直接使用单个模块。

        b. 代码提取模块类型的字符串表示,并对其进行处理,以去除前缀和后缀中的特定字符。

        c. 代码计算模块的参数数量m.np,通过遍历m_模块的参数并对其元素数量进行求和。

        d. 代码将索引i、来源索引f和类型字符串t附加到m_模块的属性中。

        e. 如果设置了verbose参数为True,代码将打印模块的相关信息,包括索引、来源索引、重复次数、参数数量和类型。

        f. 代码将f中的元素附加到savelist中,用于后续保存操作。

        g. 将构建好的模块m_添加到layers列表中,并根据索引i对ch列表进行更新,将输出通道数c2附加到ch列表中。

        h. 最终,返回构建好的模型nn.Sequential(*layers)和排序后的savelist列表。

        这段代码的目的是构建模块、记录模块信息、打印模块信息(如果设置了verbose参数),并返回最终的模型和savelist列表。

3. initialize_weights函数初始化过程:

def initialize_weights(model):
    """Initialize model weights to random values."""
    for m in model.modules():
        t = type(m)
        if t is nn.Conv2d: #注释掉,可能意味着该模块的权重已经通过其他方式初始化。
            pass  # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
        elif t is nn.BatchNorm2d: #对批归一化层的一些参数进行初始化
            m.eps = 1e-3
            m.momentum = 0.03
        elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:
            m.inplace = True #对激活函数启用原地操作

代码解析请参考代码片段中的注释。

至此,yolov8检测模型DetectionModel()的实例化过程及模型的解析构造过程介绍完成。

04-17 13:49