YOLOv7-0.1部分代码阅读笔记-train.py

train.py

train.py

目录

train.py

1.所需的库和模块

2.def train(hyp, opt, device, tb_writer=None): 

3.if __name__ == '__main__': 


1.所需的库和模块

import argparse
import logging
import math
import os
import random
import time
from copy import deepcopy
from pathlib import Path
from threading import Thread

import numpy as np
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.optim.lr_scheduler as lr_scheduler
import torch.utils.data
import yaml
from torch.cuda import amp
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.tensorboard import SummaryWriter
from tqdm import tqdm

import test  # import test.py to get mAP after each epoch
from models.experimental import attempt_load
from models.yolo import Model
from utils.autoanchor import check_anchors
from utils.datasets import create_dataloader
from utils.general import labels_to_class_weights, increment_path, labels_to_image_weights, init_seeds, \
    fitness, strip_optimizer, get_latest_run, check_dataset, check_file, check_git_status, check_img_size, \
    check_requirements, print_mutation, set_logging, one_cycle, colorstr
from utils.google_utils import attempt_download
from utils.loss import ComputeLoss, ComputeLossOTA
from utils.plots import plot_images, plot_labels, plot_results, plot_evolution
from utils.torch_utils import ModelEMA, select_device, intersect_dicts, torch_distributed_zero_first, is_parallel
from utils.wandb_logging.wandb_utils import WandbLogger, check_wandb_resume

# 在 Python 中, logging 是一个内置的库,用于跟踪事件的发生。 logging.getLogger(__name__) 是一个常用的模式,用于获取一个日志记录器(logger)实例,其中 __name__ 是当前模块的名称。
# 使用 __name__ 作为 logger 名称的好处是,它可以帮助区分不同模块的日志输出,特别是在大型项目中,这有助于更好地组织和查找日志信息。
logger = logging.getLogger(__name__)

2.def train(hyp, opt, device, tb_writer=None): 

# 这段代码定义了一个名为 train 的函数,它用于启动和执行模型的训练过程。
# 1.hyp :一个字典,包含了训练过程中的超参数。
# 2.opt :一个对象,包含了训练选项,可能是通过解析命令行参数或其他方式获得的配置。
# 3.device :指定模型和数据应该运行在哪个设备上,例如CPU或GPU。
# tb_writer :一个可选的TensorBoard写入器对象,用于记录训练过程中的指标,以便在TensorBoard中可视化。
def train(hyp, opt, device, tb_writer=None):
    # 记录超参数。这行代码使用 logger 对象记录超参数。 colorstr 函数可能用于给日志信息添加颜色,以便于区分。这里使用了一个生成器表达式来创建一个包含所有超参数键值对的字符串,并用逗号分隔。
    # def colorstr(*input): -> 它用于给字符串添加 ANSI 转义代码,以便在支持 ANSI 颜色代码的终端中以彩色输出。 -> return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
    logger.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
    # 解包选项。这行代码从 opt 对象中提取多个训练选项,并分别赋值给对应的变量。 Path(opt.save_dir) 将保存目录的字符串路径转换为 Path 对象,以便使用路径操作。其他变量如 epochs 、 batch_size 等分别保存了训练的轮数、每批样本数等配置。
    save_dir, epochs, batch_size, total_batch_size, weights, rank = \
        Path(opt.save_dir), opt.epochs, opt.batch_size, opt.total_batch_size, opt.weights, opt.global_rank

    # Directories    目录。
    # 设置权重目录。
    # 这里创建了一个用于保存模型权重的目录。 save_dir / 'weights' 构造了完整的路径, mkdir 方法创建了这个目录, parents=True 表示如果父目录不存在也会创建, exist_ok=True 表示如果目录已存在不会抛出异常。
    wdir = save_dir / 'weights'
    wdir.mkdir(parents=True, exist_ok=True)  # make dir
    # 定义模型权重文件路径。
    # 这里定义了两个路径,分别用于保存最新的模型权重( last.pt )和最佳模型权重( best.pt )。
    last = wdir / 'last.pt'
    best = wdir / 'best.pt'
    # 定义结果文件路径。这里定义了一个文件路径,用于保存训练结果。
    results_file = save_dir / 'results.txt'

    # Save run settings    保存运行设置。
    # 保存运行设置。
    # 这两行代码将超参数 hyp 和选项 opt 保存为 YAML 文件,以便后续查看和记录训练配置。
    with open(save_dir / 'hyp.yaml', 'w') as f:

        # yaml.dump(data, stream=None, Dumper=yaml.Dumper, **kwds)
        # yaml.dump 是 PyYAML 库中的一个函数,用于将 Python 对象序列化为 YAML 格式的字符串。这个函数非常适用于将配置信息或数据结构以人类可读的格式输出到文件或控制台。
        # 参数说明 :
        # data :要序列化的 Python 对象。
        # stream :一个文件流或文件路径,用于写入 YAML 数据。如果为 None ,则返回序列化后的字符串。
        # Dumper :指定一个自定义的 Dumper 类,用于自定义序列化过程。默认是 yaml.Dumper 。
        # **kwds :其他关键字参数,用于控制序列化行为,例如 default_flow_style 、 sort_keys 等。
        # 关键字参数 :
        # llow_unicode :如果为 True ,则输出 Unicode 字符而不是逃逸它们。
        # default_flow_style :控制输出的样式,默认为 None ,意味着使用块样式(block style)。如果设置为 '' ,则使用流样式(flow style)。
        # encoding :输出的编码格式,默认为 'utf-8' 。
        # explicit_start :如果为 True ,则在输出文件开始处添加 --- 。
        # indent :设置缩进级别,默认为 4。
        # sort_keys :如果为 True ,则字典键值对按照键的字母顺序排序。
        # width :设置每行的最大宽度,默认为 80。
        # 返回值 :
        # 如果 stream 参数为 None ,则 yaml.dump 返回序列化后的 YAML 格式字符串;否则,它直接将 YAML 数据写入到指定的流中。
        # 使用 yaml.dump 时,需要注意 PyYAML 的安全问题,特别是在处理来自不可信来源的 YAML 数据时,因为 YAML 可以包含任意的 Python 对象。
        # 因此,推荐使用 yaml.safe_load 和 yaml.safe_dump 来避免执行潜在的危险代码。

        yaml.dump(hyp, f, sort_keys=False)
    with open(save_dir / 'opt.yaml', 'w') as f:

        # vars(object)
        # vars() 函数在 Python 中用于获取对象的属性字典。这个字典包含了对象的大部分属性,但不包括方法和其他一些特殊的属性。对于用户自定义的对象, vars() 返回的字典包含了对象的 __dict__ 属性,这是一个包含对象所有属性的字典。
        # 参数说明 :
        # object :要获取属性字典的对象。
        # 返回值 :
        # 返回指定对象的属性字典。
        # 注意事项 :
        # vars() 对于内置类型(如 int 、 float 、 list 等)返回的是一个包含魔术方法和特殊属性的字典,这些属性通常是不可访问的。
        # 对于自定义对象, vars() 返回的是对象的 __dict__ 属性,如果对象没有定义 __dict__ ,则可能返回一个空字典或者抛出 TypeError 。
        # 在 Python 3 中, vars() 也可以用于获取内置函数的全局变量字典。
        # vars() 函数是一个内置函数,通常用于调试和访问对象的内部状态,但在处理复杂对象时应该谨慎使用,因为直接修改对象的属性可能会导致不可预测的行为。

        yaml.dump(vars(opt), f, sort_keys=False)

    # Configure    配置。
    # 配置其他设置。如果选项中 evolve 为 False ,则设置 plots 为 True ,表示需要创建训练过程中的图表。
    plots = not opt.evolve  # create plots
    # 检查是否使用CUDA。这行代码检查训练是否在GPU上进行。
    cuda = device.type != 'cpu'
    # 初始化随机种子。这行代码调用 init_seeds 函数来初始化随机种子,以确保结果的可重复性。 rank 可能是分布式训练中的进程编号。
    # def init_seeds(seed=0): -> 目的是初始化随机数生成器(RNG)的种子。这个函数的作用是确保代码的随机性是可重复的,即每次使用相同的种子时,生成的随机数序列是相同的。
    init_seeds(2 + rank)
    # 加载数据字典。
    # 这行代码从 opt.data 指定的文件中加载数据字典,这个字典包含了训练和验证数据集的信息。
    with open(opt.data) as f:
        data_dict = yaml.load(f, Loader=yaml.SafeLoader)  # data dict
    # 检查是否是COCO数据集。这行代码检查数据配置文件是否以 coco.yaml 结尾,以确定是否是COCO数据集。
    is_coco = opt.data.endswith('coco.yaml')

    # 记录 - 在检查数据集之前执行此操作。可能会更新 data_dict。
    # Logging- Doing this before checking the dataset. Might update data_dict
    # 这段代码涉及到日志记录的设置,特别是在使用 Weights & Biases (Wandb) 作为实验跟踪工具时。
    # 初始化日志记录器字典。这行代码初始化了一个字典 loggers ,用于存储不同类型的日志记录器。这里只定义了一个键 'wandb' ,其值为 None ,表示 Weights & Biases 日志记录器尚未初始化。
    loggers = {'wandb': None}  # loggers dict
    # 检查进程排名。这个条件判断表示只有当进程排名为 -1 (通常表示单进程环境)或 0 (分布式训练中的主进程)时,才会执行日志记录器的初始化。
    if rank in [-1, 0]:
        # 添加超参数到选项。将超参数 hyp 添加到选项 opt 中,这样它们可以在训练过程中被访问。
        opt.hyp = hyp  # add hyperparameters
        # 加载权重并获取 Wandb 运行 ID。如果指定的权重文件 weights 存在且以 .pt 结尾,则尝试从该文件中加载 Wandb 运行 ID。这个 ID 用于在 Wandb 中恢复或关联实验。
        run_id = torch.load(weights).get('wandb_id') if weights.endswith('.pt') and os.path.isfile(weights) else None
        # 初始化 Wandb 日志记录器。使用选项 opt 、保存目录的名称、Wandb 运行 ID 和数据字典 data_dict 来初始化 Wandb 日志记录器。
        wandb_logger = WandbLogger(opt, Path(opt.save_dir).stem, run_id, data_dict)
        # 更新日志记录器字典。将初始化的 Wandb 日志记录器添加到 loggers 字典中。
        loggers['wandb'] = wandb_logger.wandb
        # 更新数据字典。从 Wandb 日志记录器中获取更新后的数据字典,这可能包含了 Wandb 特定的修改。
        data_dict = wandb_logger.data_dict
        # 更新权重、轮数和超参数。如果 Wandb 日志记录器被激活,更新权重、轮数和超参数,这些可能在恢复实验时被 Wandb 日志记录器修改。
        if wandb_logger.wandb:
            weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp  # WandbLogger might update weights, epochs if resuming    如果恢复,WandbLogger 可能会更新权重和时期。

    # 设置类别数。根据是否是单类别( single_cls )设置类别数 nc 。如果是单类别,则 nc 为 1;否则,从数据字典中获取类别数。
    nc = 1 if opt.single_cls else int(data_dict['nc'])  # number of classes
    # 设置类别名称。根据是否是单类别和数据字典中的类别名称列表设置类别名称 names 。
    names = ['item'] if opt.single_cls and len(data_dict['names']) != 1 else data_dict['names']  # class names
    # 检查类别名称和类别数的一致性。使用 assert 语句检查类别名称的数量是否与类别数 nc 一致,如果不一致,则抛出异常。
    assert len(names) == nc, '%g names found for nc=%g dataset in %s' % (len(names), nc, opt.data)  # check    # 在 %s 中为 nc=%g 数据集找到 %g 个名称。
    # 这段代码确保了在训练开始之前,日志记录器被正确设置,并且数据字典被更新以反映任何 Wandb 特定的修改。这对于确保实验的可追踪性和可重复性至关重要。

    # Model
    # 这段代码是模型加载和配置的一部分,它处理了预训练模型的加载、权重的转移以及数据集的检查。
    # 检查预训练权重。这行代码检查提供的权重文件 weights 是否以 .pt 结尾,这通常表示 PyTorch 的模型权重文件。
    pretrained = weights.endswith('.pt')
    # 下载预训练权重(如果需要)。
    if pretrained:
        # 如果权重文件是预训练的,并且本地没有找到,这段代码会尝试下载权重文件。
        # torch_distributed_zero_first 确保在分布式训练环境中只有一个进程执行下载操作。
        # def torch_distributed_zero_first(local_rank: int):
        # -> 定义了一个名为 torch_distributed_zero_first 的上下文管理器(context manager),它用于在分布式训练中协调多个进程,确保所有进程等待某个特定的进程(通常是本地的主进程,即 local_rank 为 0 的进程)首先执行某些操作,然后其他进程再继续执行。
        with torch_distributed_zero_first(rank):
            # def attempt_download(file, repo='WongKinYiu/yolov6'): -> 尝试下载一个文件,如果该文件不存在的话。这个函数特别设计用于从 GitHub 仓库下载预训练模型文件。
            attempt_download(weights)  # download if not found locally
        # 加载预训练权重。使用 torch.load 加载预训练的权重文件,并将权重映射到指定的设备(CPU或GPU)。
        ckpt = torch.load(weights, map_location=device)  # load checkpoint
        # 创建模型实例。根据提供的配置文件 opt.cfg 或从权重文件中获取的配置创建模型实例,并将模型移动到指定的设备。
        model = Model(opt.cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # create
        # 排除不需要加载的权重。如果提供了配置文件或超参数中的锚点,并且不是从断点恢复训练,则排除某些键。
        exclude = ['anchor'] if (opt.cfg or hyp.get('anchors')) and not opt.resume else []  # exclude keys
        # 权重转换和交集。将预训练权重转换为浮点数(FP32),并与模型的状态字典进行交集操作,排除不需要的键。
        state_dict = ckpt['model'].float().state_dict()  # to FP32
        # def intersect_dicts(da, db, exclude=()):
        # -> 用于计算两个字典 da 和 db 中匹配的键和形状的交集,同时排除掉包含在 exclude 元组中的键。函数使用 da 字典中的值来构建结果字典。一个新字典,包含 da 和 db 中键匹配且形状相同的项,排除了包含 exclude 中子串的键。
        # -> return {k: v for k, v in da.items() if k in db and not any(x in k for x in exclude) and v.shape == db[k].shape}
        state_dict = intersect_dicts(state_dict, model.state_dict(), exclude=exclude)  # intersect
        # 加载权重到模型。将过滤后的权重加载到模型中, strict=False 允许模型中有不在状态字典中的参数。
        model.load_state_dict(state_dict, strict=False)  # load
        # 报告权重加载情况。记录加载的权重数量和模型状态字典中的总项数。
        logger.info('Transferred %g/%g items from %s' % (len(state_dict), len(model.state_dict()), weights))  # report    # 从 %s 转移了 %g/%g 件物品。
    # 创建非预训练模型。
    else:
        # 如果权重文件不是预训练的,根据配置文件创建模型实例。
        model = Model(opt.cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # create
    # 检查数据集。
    # 在分布式训练环境中,确保只有一个进程检查数据集。
    # def torch_distributed_zero_first(local_rank: int):
    # -> 定义了一个名为 torch_distributed_zero_first 的上下文管理器(context manager),它用于在分布式训练中协调多个进程,确保所有进程等待某个特定的进程(通常是本地的主进程,即 local_rank 为 0 的进程)首先执行某些操作,然后其他进程再继续执行。
    with torch_distributed_zero_first(rank):
        # def check_dataset(dict): -> 检查本地是否存在指定的数据集,如果不存在,则尝试下载。
        check_dataset(data_dict)  # check.
    # 获取训练和测试路径。从数据字典中获取训练和验证数据集的路径。
    train_path = data_dict['train']
    test_path = data_dict['val']
    # 这段代码确保了模型能够根据是否使用预训练权重正确加载,并且数据集被正确检查。这对于确保模型训练的顺利进行至关重要。

    # Freeze
    # 这段代码的目的是冻结(即不更新)模型中某些指定层的参数。在深度学习中,冻结某些层的参数是一种常见的做法,特别是在使用预训练模型时,我们可能只想训练模型的某些部分。
    # 初始化冻结列表。这行代码初始化一个空列表 freeze ,用于存储需要冻结的参数名称。
    freeze = []  # parameter names to freeze (full or partial)
    # 遍历模型参数。这行代码遍历模型的所有参数, k 是参数的名称, v 是对应的参数对象。
    for k, v in model.named_parameters():
        # 设置参数的梯度。默认情况下,所有参数的 requires_grad 属性被设置为 True ,这意味着在反向传播时会计算这些参数的梯度,从而在优化过程中更新它们。
        v.requires_grad = True  # train all layers
        # 检查是否需要冻结参数。
        # 这段代码检查参数名称 k 是否包含在 freeze 列表中的任何字符串。如果是,那么打印出正在冻结的参数名称,并将该参数的 requires_grad 属性设置为 False ,这样在训练过程中就不会更新这个参数。
        if any(x in k for x in freeze):
            print('freezing %s' % k)
            v.requires_grad = False
    # 注意事项 :
    # 确保 freeze 列表中的字符串与模型参数的实际名称匹配。参数名称通常是由模型定义中的层名称决定的。
    # 打印语句 print('freezing %s' % k) 用于调试,可以帮助你确认哪些参数被冻结。
    # 这种方法只影响参数的梯度计算,如果 requires_grad 设置为 False ,则对应的参数在训练过程中不会被更新。
    # 这段代码提供了一种灵活的方式来控制模型中哪些参数应该在训练中更新,哪些应该保持不变。这对于迁移学习和微调预训练模型特别有用。

    # Optimizer
    # 这段代码配置了一个优化器,用于在深度学习训练中更新模型的参数。它涉及到设置不同的参数组,并对它们应用不同的优化策略。
    # 设置名义批量大小和梯度累积。
    # 这里设置了名义批量大小 nbs ,并 计算了在优化前需要累积多少个小批量的损失。这是为了模拟在更大批量上训练的效果。
    nbs = 64  # nominal batch size
    accumulate = max(round(nbs / total_batch_size), 1)  # accumulate loss before optimizing    在优化之前累积损失。
    # 调整权重衰减。
    # 根据实际批量大小和累积次数调整超参数中的权重衰减值,并记录调整后的值。
    hyp['weight_decay'] *= total_batch_size * accumulate / nbs  # scale weight_decay
    logger.info(f"Scaled weight_decay = {hyp['weight_decay']}")

    # 初始化参数组。
    # 初始化三个列表,用于存储不同的参数组。
    # pg0 :用于不衰减的参数(如批量归一化层的权重)。
    # pg1 :用于应用衰减的参数(如卷积层的权重)。
    # pg2 :用于偏置项。
    pg0, pg1, pg2 = [], [], []  # optimizer parameter groups
    # 遍历模型模块并分配参数组。遍历模型的所有模块,并根据模块类型将参数分配到不同的参数组。 k 是模块的名称, v 是模块本身。
    for k, v in model.named_modules():
        # 处理偏置项( pg2 )。
        if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):
            # 如果模块 v 有 bias 属性,并且 bias 是一个 nn.Parameter 对象,那么将这个偏置项添加到 pg2 列表中。在优化器中,这些偏置项通常不应用权重衰减。
            pg2.append(v.bias)  # biases
        # 处理批量归一化层( pg0 )。
        if isinstance(v, nn.BatchNorm2d):
            # 如果模块 v 是 nn.BatchNorm2d 类型(即批量归一化层),那么将其权重添加到 pg0 列表中。这些权重在优化过程中通常不应用权重衰减。
            pg0.append(v.weight)  # no decay
        # 处理其他权重( pg1 )。
        elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):
            # 如果模块 v 有 weight 属性,并且 weight 是一个 nn.Parameter 对象,那么将这个权重添加到 pg1 列表中。这些权重在优化过程中将应用权重衰减。
            pg1.append(v.weight)  # apply decay
        # 处理隐式层( pg0 )。
        if hasattr(v, 'im'):
            if hasattr(v.im, 'implicit'):      
                # 如果模块 v 有 im 属性,这可能指的是隐式层(例如在某些变分自编码器或生成模型中)。如果 im 属性有 implicit 属性,那么将 v.im.implicit 添加到 pg0 列表中。
                pg0.append(v.im.implicit)
            else:
                # 如果没有 implicit 属性,但 im 是一个包含多个隐式层的集合,那么遍历这些层并将它们的 implicit 属性添加到 pg0 列表中。
                for iv in v.im:
                    pg0.append(iv.implicit)
        # 这段代码的目的是将模型中的参数根据其 特性 分配到不同的参数组中,以便在训练过程中应用不同的优化策略。例如,批量归一化层的权重和偏置项通常不进行权重衰减,而其他层的权重则应用权重衰减。这种策略有助于模型训练的稳定性和收敛性。
        if hasattr(v, 'imc'):
            if hasattr(v.imc, 'implicit'):           
                pg0.append(v.imc.implicit)
            else:
                for iv in v.imc:
                    pg0.append(iv.implicit)
        if hasattr(v, 'imb'):
            if hasattr(v.imb, 'implicit'):           
                pg0.append(v.imb.implicit)
            else:
                for iv in v.imb:
                    pg0.append(iv.implicit)
        if hasattr(v, 'imo'):
            if hasattr(v.imo, 'implicit'):           
                pg0.append(v.imo.implicit)
            else:
                for iv in v.imo:
                    pg0.append(iv.implicit)
        if hasattr(v, 'ia'):
            if hasattr(v.ia, 'implicit'):           
                pg0.append(v.ia.implicit)
            else:
                for iv in v.ia:
                    pg0.append(iv.implicit)
        if hasattr(v, 'attn'):
            if hasattr(v.attn, 'logit_scale'):   
                pg0.append(v.attn.logit_scale)
            if hasattr(v.attn, 'q_bias'):   
                pg0.append(v.attn.q_bias)
            if hasattr(v.attn, 'v_bias'):  
                pg0.append(v.attn.v_bias)
            if hasattr(v.attn, 'relative_position_bias_table'):  
                pg0.append(v.attn.relative_position_bias_table)
        if hasattr(v, 'rbr_dense'):
            if hasattr(v.rbr_dense, 'weight_rbr_origin'):  
                pg0.append(v.rbr_dense.weight_rbr_origin)
            if hasattr(v.rbr_dense, 'weight_rbr_avg_conv'): 
                pg0.append(v.rbr_dense.weight_rbr_avg_conv)
            if hasattr(v.rbr_dense, 'weight_rbr_pfir_conv'):  
                pg0.append(v.rbr_dense.weight_rbr_pfir_conv)
            if hasattr(v.rbr_dense, 'weight_rbr_1x1_kxk_idconv1'): 
                pg0.append(v.rbr_dense.weight_rbr_1x1_kxk_idconv1)
            if hasattr(v.rbr_dense, 'weight_rbr_1x1_kxk_conv2'):   
                pg0.append(v.rbr_dense.weight_rbr_1x1_kxk_conv2)
            if hasattr(v.rbr_dense, 'weight_rbr_gconv_dw'):   
                pg0.append(v.rbr_dense.weight_rbr_gconv_dw)
            if hasattr(v.rbr_dense, 'weight_rbr_gconv_pw'):   
                pg0.append(v.rbr_dense.weight_rbr_gconv_pw)
            if hasattr(v.rbr_dense, 'vector'):   
                pg0.append(v.rbr_dense.vector)

    # 创建优化器。
    # 根据选项 opt.adam 决定使用 Adam 优化器还是 SGD 优化器,并设置初始学习率和动量。
    if opt.adam:
        optimizer = optim.Adam(pg0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))  # adjust beta1 to momentum
    else:
        optimizer = optim.SGD(pg0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)

    # 添加参数组。
    # 将 pg1 和 pg2 参数组添加到优化器中, pg1 应用权重衰减,而 pg2 不应用。
    optimizer.add_param_group({'params': pg1, 'weight_decay': hyp['weight_decay']})  # add pg1 with weight_decay
    optimizer.add_param_group({'params': pg2})  # add pg2 (biases)
    # 记录优化器组信息。记录不同参数组的大小,以便了解优化器的配置。
    logger.info('Optimizer groups: %g .bias, %g conv.weight, %g other' % (len(pg2), len(pg1), len(pg0)))    # 优化器组:%g .bias、%g conv.weight、%g other。
    # 清理。删除不再需要的参数组列表,以释放内存。
    del pg0, pg1, pg2
# 这段代码通过精心设计的参数分组和优化器设置,为模型训练提供了灵活的配置。这对于调整训练动态和提高模型性能至关重要。

    # Scheduler https://arxiv.org/pdf/1812.01187.pdf
    # https://pytorch.org/docs/stable/_modules/torch/optim/lr_scheduler.html#OneCycleLR
    # 这段代码是设置学习率调度器(scheduler)的一部分,它根据配置选择不同的学习率调整策略。学习率调度器在训练深度学习模型时非常重要,因为它可以帮助模型在训练过程中动态调整学习率,从而优化训练效果。
    # 设置线性学习率函数。
    if opt.linear_lr:
        # 如果选项 opt.linear_lr 为 True ,则定义一个线性学习率衰减函数 lf 。这个函数随着训练进度 x (当前epoch索引)的变化,线性地将学习率从初始值减少到 hyp['lrf'] (一个超参数,表示最终的学习率因子)。
        lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp['lrf']) + hyp['lrf']  # linear
    # 设置余弦退火学习率函数。
    else:
        # 如果 opt.linear_lr 为 False ,则使用 one_cycle 函数定义一个余弦退火学习率调度策略。这个策略在训练初期快速降低学习率,在训练后期缓慢降低至 hyp['lrf'] 。
        # def one_cycle(y1=0.0, y2=1.0, steps=100):
        # -> one_cycle 的函数,它生成一个 lambda 函数,该 lambda 函数实现了一个正弦波形的上升和下降周期,从 y1 到 y2 。返回一个 lambda 函数,该函数接受一个参数 x (表示周期中的步数),并返回一个浮点数,表示在该步数时的值。
        # -> return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
        lf = one_cycle(1, hyp['lrf'], epochs)  # cosine 1->hyp['lrf']
    
    # torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1, verbose=False)
    # torch.optim.lr_scheduler.LambdaLR 是 PyTorch 中的一个学习率调度器,它允许你根据一个给定的函数来调整学习率。这个函数可以自定义,以适应不同的训练需求和策略。
    # 参数 :
    # optimizer :被包装的优化器。
    # lr_lambda :一个函数,它接受一个整数参数 epoch ,并返回一个乘法因子。这个因子将用于调整学习率。也可以是一个函数列表,其中每个函数对应于优化器中的一个参数组。
    # last_epoch :整数,表示最后一个epoch的索引。默认为 -1 ,意味着从初始学习率开始。
    # verbose :布尔值,如果为 True ,则在每次更新时打印一条消息到标准输出。默认为 False 。
    # 工作原理 :
    # LambdaLR 调度器将每个参数组的学习率设置为初始学习率乘以给定函数的值。这个函数可以是任何形式,例如,可以是一个简单的线性衰减、指数衰减,或者更复杂的自定义函数。

    # 创建学习率调度器。使用 PyTorch 的 lr_scheduler.LambdaLR 创建一个学习率调度器,它接受优化器 optimizer 和学习率调整函数 lf 作为参数。
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)
    # 绘制学习率调度图(注释掉的部分)。这行代码被注释掉了,但它暗示了有一个函数 plot_lr_scheduler 可以用来绘制学习率随训练进度变化的曲线,这有助于直观地理解学习率调度策略。
    # plot_lr_scheduler(optimizer, scheduler, epochs)
    # 额外说明 :
    # opt.linear_lr :这是一个布尔选项,用于选择是否使用线性学习率衰减。
    # hyp['lrf'] :这是超参数字典中的一个条目,表示最终的学习率因子。
    # one_cycle :这是一个自定义函数,用于生成余弦退火学习率调度策略。这个函数参考了 PyTorch 的 OneCycleLR ,但在这里被简化为 one_cycle 。
    # lr_scheduler.LambdaLR :这是 PyTorch 中的一个学习率调度器,它允许使用一个 lambda 函数来动态调整学习率。
    # 这段代码展示了如何在 PyTorch 中根据配置选择和设置不同的学习率调度策略,这对于训练深度学习模型和调整其性能至关重要。

    # EMA
    # 这段代码是实现模型的指数移动平均(Exponential Moving Average, EMA)的一部分。EMA 是一种技术,用于维护模型参数的平滑版本,这在训练深度学习模型时特别有用,因为它可以帮助减少训练过程中的噪声,提高模型的泛化能力。
    # class ModelEMA:
    # -> ModelEMA 的类,它实现了模型指数移动平均(Exponential Moving Average, EMA)的功能。EMA是一种技术,用于计算模型参数的平滑版本,这在某些训练方案中是必要的,特别是在训练的早期阶段。
    # -> def __init__(self, model, decay=0.9999, updates=0):
    # 这行代码检查 rank 变量的值。 rank 通常用于分布式训练环境中,表示当前进程的编号。如果 rank 是 -1 (表示单进程环境)或 0 (表示分布式训练中的第一个进程),则创建一个 ModelEMA 实例,将原始模型 model 传递给它。否则, ema 被设置为 None 。
    ema = ModelEMA(model) if rank in [-1, 0] else None
    # EMA 的作用 :
    # 平滑参数 :EMA 通过计算指数加权平均来更新参数,这有助于平滑训练过程中的参数波动。
    # 减少噪声 :在训练过程中,EMA 参数更新可以减少单个数据批次的随机性带来的噪声。
    # 提高泛化能力 :使用 EMA 参数的模型在测试时往往表现得更加稳定,因为它减少了过拟合的风险。
    # ModelEMA 类 ModelEMA 是一个自定义类,它封装了 EMA 的逻辑。这个类包含以下功能 :
    # 更新 EMA 参数 :在每个训练步骤后,根据一定的衰减率(例如,0.999)更新 EMA 参数。
    # 应用 EMA 参数 :在评估或测试时,将 EMA 参数应用到模型中,以获得更稳定的表现。
    # 恢复 EMA 参数 :在训练完成后,可以将 EMA 参数恢复到原始模型中,以提高模型的最终性能。
    # 使用场景 :
    # 训练期间 :在每个 epoch 或 batch 结束时,更新 EMA 参数。
    # 评估期间 :使用 EMA 参数进行模型评估,以获得更准确的性能指标。
    # 模型保存 :在保存模型时,可以选择保存 EMA 参数,以便在将来的推理或继续训练中使用。
    # 这段代码展示了如何在训练脚本中集成 EMA,这是一个提高模型性能和稳定性的有效技术。

    # Resume    恢复。
    # 这段代码处理了从预训练模型(checkpoint)恢复训练的过程,包括优化器状态、EMA(指数移动平均)参数、训练结果和当前epoch的恢复。
    # 初始化起始epoch和最佳适应度。这里初始化了两个变量, start_epoch 用于记录从哪个epoch开始继续训练, best_fitness 用于记录之前训练过程中的最佳性能指标。
    start_epoch, best_fitness = 0, 0.0
    # 恢复优化器状态。
    if pretrained:
        # 如果checkpoint中包含优化器状态,则加载该状态,并更新最佳适应度值。
        # Optimizer
        # 检查优化器状态。这行代码检查在加载的checkpoint字典 ckpt 中是否有优化器状态信息。 ckpt 是一个包含模型训练过程中各种状态信息的字典,通常在保存模型权重时一并保存。
        if ckpt['optimizer'] is not None:
            # 加载优化器状态。如果checkpoint中包含优化器状态,则使用 load_state_dict 方法将这些状态加载到当前优化器对象 optimizer 中。这允许优化器在恢复训练时保持之前的状态,包括学习率、动量等参数。
            optimizer.load_state_dict(ckpt['optimizer'])
            # 更新最佳适应度值。加载checkpoint中保存的最佳适应度值( best_fitness ),这个值通常用于记录模型在验证集上的最佳性能,以便在恢复训练时知道之前的最佳表现。
            best_fitness = ckpt['best_fitness']
        # 目的和重要性 :
        # 恢复优化器状态 :这一步骤确保了训练过程中优化器的参数(如学习率、动量等)能够从中断点继续,而不是从头开始。
        # 保持最佳性能记录 :通过恢复最佳适应度值,可以确保在恢复训练时不会丢失之前的最佳模型性能记录,这对于模型选择和早停策略(early stopping)非常重要。
        # 注意事项 :
        # 确保checkpoint文件是在与当前训练设置兼容的环境中保存的,因为不同的训练配置可能导致优化器状态不兼容。
        # 在使用 load_state_dict 之前,需要确保优化器 optimizer 已经被正确初始化,并且其参数与checkpoint中的参数相匹配。
        # 如果在分布式训练环境中,需要确保所有进程都正确加载了优化器状态。

        # 恢复EMA参数。如果使用了EMA并且checkpoint中包含EMA参数,则加载这些参数,并更新EMA的更新次数。
        # EMA
        # 检查 EMA 和检查点中的 EMA 状态。这行代码首先检查是否存在一个 EMA 实例( ema ),并且检查点( ckpt )中是否包含 EMA 状态( 'ema' )。如果两者都存在,则继续执行下面的代码。
        if ema and ckpt.get('ema'):
            # 加载 EMA 状态字典。这行代码加载检查点中的 EMA 状态字典到当前 EMA 实例中。
            # ckpt['ema'].float().state_dict() 将检查点中的 EMA 参数转换为浮点数(如果它们不是浮点数的话),然后获取其状态字典。
            # ema.ema.load_state_dict() 方法用于将这些参数加载到 EMA 实例中。
            ema.ema.load_state_dict(ckpt['ema'].float().state_dict())
            # 更新 EMA 更新次数。这行代码将检查点中的 updates 值(表示 EMA 更新的次数)更新到当前 EMA 实例中。这对于正确维护 EMA 参数的指数衰减至关重要。
            ema.updates = ckpt['updates']
        # EMA 的目的和重要性 :
        # 平滑模型参数 :EMA 通过计算指数加权平均来更新模型参数,这有助于平滑训练过程中的参数波动,减少过拟合。
        # 提高泛化能力 :使用 EMA 参数的模型在测试时往往表现得更加稳定,因为它减少了单个数据批次的随机性带来的噪声。
        # 恢复训练状态 :在训练过程中断后,EMA 允许从中断点恢复,继续之前的平滑参数更新。
        # 注意事项 :
        # 确保检查点文件是在与当前训练设置兼容的环境中保存的,因为不同的训练配置可能导致 EMA 状态不兼容。
        # 在使用 load_state_dict 之前,需要确保 EMA 实例 ema 已经被正确初始化。
        # ckpt['ema'].float() 确保了 EMA 参数与当前设备(如 GPU)的计算类型(浮点数)兼容。

        # 恢复训练结果。如果checkpoint中包含训练结果,则将这些结果写入到 results_file 指定的文件中。
        # Results

        # value = dict.get(key, default=None)
        # dict.get() 是 Python 字典( dict )类型提供的一个方法,用于从字典中获取指定键(key)对应的值(value)。如果键不存在于字典中,它将返回一个默认值,这个默认值可以是调用方法时指定的,如果没有指定,则默认为 None 。
        # 参数说明 :
        # key :要检索的键。
        # default :如果键不在字典中,返回的默认值。如果未提供此参数,且键不存在时,默认返回 None 。
        # 返回值 :
        # 返回字典中键 key 对应的值,如果键不存在,则返回 default 指定的值或 None 。
        # 注意事项 :
        # 使用 dict.get() 方法可以避免在访问字典键时出现 KeyError 异常,当键不存在时,它提供了一种更安全的访问方式。
        # 如果需要在键不存在时执行某些操作,可以在 get() 方法中设置一个特定的默认值,或者根据返回的 None 值来决定后续操作。

        # 检查检查点中是否有训练结果。这行代码使用 ckpt.get('training_results') 方法来尝试从检查点字典 ckpt 中获取键 'training_results' 对应的值。如果该值存在且不为 None ,则继续执行下面的代码。
        if ckpt.get('training_results') is not None:

            # path.write_text(data, encoding='utf-8', errors='strict', newline=None)
            # pathlib.Path 对象的 write_text() 方法,这是在 Python 3.6 及以上版本中引入的。
            # 参数说明 :
            # data :要写入文件的文本数据。
            # encoding :用于编码文本数据的编码格式,默认为 'utf-8' 。
            # errors :指定如何处理编码错误,可选的值包括 'strict' 、 'ignore' 、 'replace' 等,默认为 'strict' 。
            # newline :指定换行符的处理方式,可以是 '' 、 '\n' 、 '\r' 、 '\r\n' 之一,或者 None (默认值),表示让系统决定。
            # 返回值 :
            # 该方法没有返回值,它直接将文本数据写入到指定的文件路径中。

            # 将训练结果写入文件。这行代码将检查点中的训练结果( ckpt['training_results'] )写入到 results_file 指定的文件中。 results_file 是一个文件路径对象, write_text 方法用于将字符串写入文件。这里假设 results_file 已经被定义并指向了正确的文件路径。
            results_file.write_text(ckpt['training_results'])  # write results.txt
        # 目的和重要性 :
        # 保存训练历史 :训练结果通常包含了每个epoch的性能指标,如损失值、准确率等,这些信息对于分析模型性能和调试模型非常有用。
        # 恢复训练进度 :在训练过程中断后,保存的训练结果可以帮助恢复训练进度,继续之前的训练。
        # 注意事项 :
        # 确保 results_file 指向的文件路径是可写的,且有足够的权限进行文件操作。
        # 确保检查点中的 training_results 是字符串格式,因为 write_text 方法期望接收一个字符串参数。
        # 如果 results_file 已经存在, write_text 方法会覆盖原有内容。如果需要追加内容,可以考虑使用 append 模式打开文件。

        # Epochs
        # 设置起始epoch。
        # 设置起始epoch为checkpoint中记录的epoch加1。
        start_epoch = ckpt['epoch'] + 1
        # 断言检查和额外训练信息。如果设置了 opt.resume ,则断言 start_epoch 大于0,表示有训练可以恢复。如果指定的总epoch数小于已训练的epoch数,则记录额外微调的epoch数,并更新总epoch数。
        # 验证恢复条件。
        # 如果选项 opt.resume 为 True ,则使用 assert 语句确保 start_epoch 大于0,这意味着训练尚未完成,有内容可以恢复。如果 start_epoch 不大于0,则断言失败,并显示一条消息,指出训练已经完成,无需恢复。
        if opt.resume:
            assert start_epoch > 0, '%s training to %g epochs is finished, nothing to resume.' % (weights, epochs)    # %s 训练至 %g 个时期已完成,无需恢复。
        # 记录训练信息并调整训练周期。
        # 如果指定的总训练周期 epochs 小于已经完成的周期 start_epoch ,则使用 logger.info 记录一条信息,指出模型已经训练了多少周期,并且将进行额外的微调周期。然后,将已完成的周期数加到总周期数上,以确保模型继续训练指定的额外周期数。
        if epochs < start_epoch:
            logger.info('%s has been trained for %g epochs. Fine-tuning for %g additional epochs.' %    # %s 已训练了 %g 个周期。正在对另外 %g 个周期进行微调。
                        (weights, ckpt['epoch'], epochs))
            epochs += ckpt['epoch']  # finetune additional epochs    微调额外的时期。
        # 目的和重要性 :
        # 确保训练连续性 :这段代码确保了从检查点恢复训练时,训练周期是连续的,不会因为重新开始而丢失之前的训练进度。
        # 微调额外周期 :如果需要在预训练的基础上进行额外的训练周期,这段代码允许调整总训练周期,以包含这些额外的微调周期。
        # 注意事项 :
        # opt.resume 选项用于控制是否从检查点中恢复训练。
        # start_epoch 表示从检查点中恢复的训练起始周期。
        # weights 是模型权重文件的路径,用于在日志信息中标识模型。
        # ckpt['epoch'] 是检查点中记录的已完成训练周期数。
        # logger 是用于记录日志信息的工具,通常在训练脚本中初始化。

        # 清理。删除不再需要的变量,以释放内存。
        del ckpt, state_dict
    # 注意事项 :
    # 这段代码假设checkpoint文件包含了所有需要恢复的信息,包括优化器状态、EMA参数、训练结果和当前epoch。
    # opt.resume 选项用于控制是否从checkpoint中恢复训练。
    # ema.updates 记录了EMA参数的更新次数,这对于EMA的正确更新非常重要。
    # results_file 是一个文件路径,用于存储训练结果。
    # 这段代码展示了如何在PyTorch中从预训练模型恢复训练过程,这对于继续训练或微调模型非常有用。

    # Image sizes
    # 这段代码涉及到图像尺寸的处理,特别是在目标检测模型中,图像尺寸需要与模型的stride(步长)兼容。
    # 计算网格尺寸( gs )。
    # 这行代码计算模型的最大步长(stride),并将其转换为整数。 model.stride.max() 获取模型中所有层的最大步长值, int() 确保结果为整数。 gs 代表网格尺寸,它必须是模型步长的倍数,同时也不能小于32(作为默认的最小网格尺寸)。
    gs = max(int(model.stride.max()), 32)  # grid size (max stride)
    # 获取检测层的数量( nl )。这行代码获取模型最后一个层的 nl 属性,它表示模型中用于目标检测的层的数量。这个值可能用于后续的缩放操作,比如调整超参数 hyp['obj'] 。
    nl = model.model[-1].nl  # number of detection layers (used for scaling hyp['obj'])
    # 验证和调整图像尺寸( imgsz 和 imgsz_test )。这行代码遍历 opt.img_size 列表中的所有图像尺寸值,并使用 check_img_size 函数检查和调整它们,确保图像尺寸是 gs 的倍数。
    # def check_img_size(img_size, s=32):
    # -> 验证给定的图像尺寸 img_size 是否是步长 s 的倍数。如果不是,函数将调整 img_size 到最接近的、大于或等于 img_size 的 s 的倍数,并打印一条警告信息。返回调整后的图像尺寸 new_size 。
    # -> return new_size
    imgsz, imgsz_test = [check_img_size(x, gs) for x in opt.img_size]  # verify imgsz are gs-multiples
    # 目的和重要性 :
    # 网格尺寸( gs ) :在目标检测模型中,图像尺寸通常需要是模型步长的倍数,以确保模型可以有效地处理图像并进行预测。
    # 检测层数量( nl ) :这个值可能用于调整模型的超参数,比如目标检测的置信度阈值,以适应不同数量的检测层。
    # 图像尺寸验证和调整 :确保图像尺寸与模型的步长兼容,这对于模型的训练和推理至关重要,因为不兼容的图像尺寸可能导致模型性能下降或错误。
    # 注意事项 :
    # check_img_size 函数需要能够正确处理图像尺寸的验证和调整。
    # opt.img_size 应该包含一个或多个图像尺寸值,这些值将在训练和测试中使用。
    # 调整图像尺寸可能会影响模型的输入数据分布,从而影响模型性能。
    # 这段代码展示了如何在模型训练前处理和验证图像尺寸,以确保它们与模型的步长兼容。

    # 这段代码处理了在不同训练模式下对模型的配置,包括使用数据并行(DataParallel)和同步批量归一化(SyncBatchNorm)。
    # DP mode
    # 数据并行(DataParallel)。
    # 检查条件并应用数据并行。
    # cuda :一个布尔值,指示是否使用CUDA(即GPU)。
    # rank == -1 :通常表示这不是分布式训练环境, rank 是分布式训练中的进程索引。
    # torch.cuda.device_count() > 1 :检查是否有多于一个GPU可用。
    # 如果满足以上条件,使用 torch.nn.DataParallel 包装模型,这样可以在多个GPU上并行训练模型。
    if cuda and rank == -1 and torch.cuda.device_count() > 1:
        model = torch.nn.DataParallel(model)
    # 在使用 torch.nn.DataParallel 时,模型的所有参数和缓存将会被复制到每个GPU上,这可能会导致内存消耗增加。
    # 数据并行:通过在多个GPU上并行训练模型,可以加速训练过程,并允许模型使用更大的批量大小,这有助于提高模型性能和稳定性。

    # SyncBatchNorm
    # 同步批量归一化(SyncBatchNorm)。
    # 检查条件并应用同步批量归一化。
    # opt.sync_bn :一个布尔值,指示是否使用同步批量归一化。
    # cuda :同上,指示是否使用CUDA。
    # rank != -1 :表示这是一个分布式训练环境。
    # 如果满足以上条件,使用 torch.nn.SyncBatchNorm.convert_sync_batchnorm 将模型中的批量归一化层转换为同步批量归一化层,然后将其移动到指定的设备( device )上。
    if opt.sync_bn and cuda and rank != -1:
        model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
        # 记录日志信息,表明正在使用同步批量归一化。
        logger.info('Using SyncBatchNorm()')
    # 同步批量归一化需要在所有进程中同步,可能会增加通信开销,但在大规模分布式训练中,它有助于提高模型的收敛性和性能。
    # 同步批量归一化:在分布式训练环境中,同步批量归一化确保了不同GPU上的批量归一化层使用相同的统计数据进行归一化,这有助于保持模型性能的一致性。

    # Trainloader
    # 这段代码涉及到数据加载和验证的过程,特别是在深度学习模型训练中。
    # create_dataloader 函数用于创建数据加载器( dataloader )和数据集( dataset )对象。
    # def create_dataloader(path, imgsz, batch_size, stride, opt, hyp=None, augment=False, cache=False, pad=0.0, rect=False, rank=-1, world_size=1, workers=8, image_weights=False, quad=False, prefix=''):
    # -> 它用于创建并返回一个 PyTorch 数据加载器(dataloader),这个加载器可以用于深度学习模型的训练过程中加载数据。函数接受多个参数,用于配置数据加载器的行为。返回数据加载器和数据集。函数返回创建的数据加载器和数据集对象。
    # -> return dataloader, dataset
    dataloader, dataset = create_dataloader(train_path, imgsz, batch_size, gs, opt,
                                            hyp=hyp, augment=True, cache=opt.cache_images, rect=opt.rect, rank=rank,
                                            world_size=opt.world_size, workers=opt.workers,
                                            image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: '))
    # 验证标签类别。这行代码使用 NumPy 的 concatenate 函数将数据集中的所有标签数组连接起来,然后找到最大的标签类别( mlc )。
    mlc = np.concatenate(dataset.labels, 0)[:, 0].max()  # max label class
    # 获取批次数量。这行代码获取数据加载器中的批次总数。
    nb = len(dataloader)  # number of batches
    # 验证标签类别是否超出范围。这行代码使用 assert 语句验证最大的标签类别 mlc 是否小于类别总数 nc 。如果 mlc 大于或等于 nc ,则断言失败,并显示一条错误消息,指出标签类别超出了允许的范围。
    assert mlc < nc, 'Label class %g exceeds nc=%g in %s. Possible class labels are 0-%g' % (mlc, nc, opt.data, nc - 1)    # 标签类别 %g 超出 %s 中的 nc=%g。可能的类别标签为 0-%g。
    # 数据加载器 :创建数据加载器是为了在训练过程中高效地加载和处理数据。
    # 标签验证 :验证标签类别是否超出范围是重要的,因为这可以确保模型的标签类别与数据集中的实际类别相匹配,避免训练过程中出现错误。

    # Process 0
    # 这段代码涉及到在训练深度学习模型时,为 测试集 创建数据加载器(test loader),并进行一些测试前的准备工作,包括标签统计、绘制标签分布图、检查和调整锚点(anchors)等。
    # 创建测试数据加载器(Test Dataloader)。
    if rank in [-1, 0]:
        testloader = create_dataloader(test_path, imgsz_test, batch_size * 2, gs, opt,  # testloader
                                       hyp=hyp, cache=opt.cache_images and not opt.notest, rect=True, rank=-1,
                                       world_size=opt.world_size, workers=opt.workers,
                                       pad=0.5, prefix=colorstr('val: '))[0]

        # 标签统计和绘制标签分布图。
        if not opt.resume:
            # 如果不是从检查点恢复训练( opt.resume 为 False ),则将数据集中的所有标签数组连接起来,并提取类别信息。
            labels = np.concatenate(dataset.labels, 0)
            c = torch.tensor(labels[:, 0])  # classes
            # cf = torch.bincount(c.long(), minlength=nc) + 1.  # frequency
            # model._initialize_biases(cf.to(device))
            # plots 变量控制是否生成图表。
            if plots:
                #plot_labels(labels, names, save_dir, loggers)
                # 如果 plots 为 True 且 tb_writer (TensorBoard写入器)存在,则使用 tb_writer.add_histogram 方法将类别分布添加到TensorBoard中。
                if tb_writer:
                    tb_writer.add_histogram('classes', c, 0)

            # 检查和调整锚点(Anchors)。
            # Anchors
            # 如果 opt.noautoanchor 为 False ,则调用 check_anchors 函数检查和调整锚点。
            if not opt.noautoanchor:
                # check_anchors 函数根据数据集的统计信息自动调整锚点的尺寸和比例。
                # thr 是锚点的阈值, imgsz 是图像尺寸。
                # def check_anchors(dataset, model, thr=4.0, imgsz=640): -> 用于检查在目标检测模型中使用的锚点(anchors)是否适合数据集,并在必要时重新计算锚点。
                check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz)
            # 模型精度调整。这行代码将模型的精度从单精度(float32)降低到半精度(float16),然后再转换回单精度。这样做可以减少模型参数的内存占用,同时在某些情况下可以加速训练。
            model.half().float()  # pre-reduce anchor precision
    # 测试数据加载器 :创建测试数据加载器是为了在模型评估阶段高效地加载测试数据。
    # 标签统计 :统计标签分布有助于了解数据集的类别分布情况,对于不平衡数据集的处理非常重要。
    # 锚点调整 :在目标检测任务中,锚点的调整对于模型性能至关重要。
    # 模型精度调整 :降低模型精度可以减少内存占用并加速训练,尤其是在GPU资源有限的情况下。
    # 这段代码展示了在训练前对测试数据进行准备和对模型进行配置的过程。

    # DDP mode
    # 这段代码是用于在分布式数据并行(Distributed Data Parallel,简称DDP)模式下包装模型的。DDP是PyTorch中用于加速训练的一种并行计算策略,它允许模型在多个GPU上并行训练。
    # 检查是否启用DDP。
    # 这行代码检查是否同时满足以下条件 。cuda :一个布尔值,指示是否使用CUDA(即GPU)。 rank != -1 : rank 是分布式训练中的进程索引, -1 通常表示单进程环境。
    if cuda and rank != -1:
        # 包装模型以使用DDP。
        # model :要包装的原始模型。
        # device_ids=[opt.local_rank] :指定模型应该运行在哪个GPU上, opt.local_rank 是当前进程的本地GPU索引。
        # output_device=opt.local_rank :指定输出应该发送到哪个设备,通常与 device_ids 相同。
        # find_unused_parameters :这是一个特殊的参数,用于处理模型中未使用的参数。
        # 在某些情况下,如模型包含 nn.MultiheadAttention 层,DDP可能无法正确处理所有参数,因此需要设置 find_unused_parameters=True 来确保所有参数都被正确更新。这个参数的值是通过检查模型中是否包含 nn.MultiheadAttention 层来确定的。
        model = DDP(model, device_ids=[opt.local_rank], output_device=opt.local_rank,
                    # nn.MultiheadAttention incompatibility with DDP https://github.com/pytorch/pytorch/issues/26698
                    find_unused_parameters=any(isinstance(layer, nn.MultiheadAttention) for layer in model.modules()))
    # 分布式训练 :DDP可以显著加速训练过程,特别是在大型模型和大规模数据集上。
    # 参数同步 :DDP确保所有GPU上的模型参数在每次迭代后都被同步,从而保持模型的一致性。
    # 处理特殊层 :对于 nn.MultiheadAttention 这样的特殊层,可能需要额外的参数处理,以确保它们在DDP环境下正常工作。
    # find_unused_parameters=True 可能会增加一些额外的计算开销,但它确保了模型参数的正确更新。
    # 这段代码展示了如何在PyTorch中使用DDP来包装模型,以便在分布式训练环境中使用。

    # Model parameters
    # 这段代码涉及到模型参数的设置和调整,特别是在训练目标检测模型时。
    # 调整超参数。
    # 将与边界框相关的超参数乘以一个因子,该因子是3除以检测层的数量( nl )。
    hyp['box'] *= 3. / nl  # scale to layers
    # 将与类别相关的超参数乘以一个因子,该因子是类别数( nc )除以80再乘以3,然后除以检测层的数量。
    hyp['cls'] *= nc / 80. * 3. / nl  # scale to classes and layers
    # 将与目标检测相关的超参数乘以一个因子,该因子是图像尺寸( imgsz )除以640的平方,再乘以3,然后除以检测层的数量。
    hyp['obj'] *= (imgsz / 640) ** 2 * 3. / nl  # scale to image size and layers
    # 将标签平滑超参数设置为训练选项( opt )中指定的值。
    hyp['label_smoothing'] = opt.label_smoothing
    # 附加模型参数。
    # 将类别数( nc )附加到模型。
    model.nc = nc  # attach number of classes to model
    # 将超参数( hyp )附加到模型。
    model.hyp = hyp  # attach hyperparameters to model
    # 设置模型的IoU损失比例为1.0,这意味着目标损失( obj_loss )可以是1.0或IoU损失。
    model.gr = 1.0  # iou loss ratio (obj_loss = 1.0 or iou)
    # 计算类别权重。
    # 计算类别权重,将数据集中的标签转换为权重,然后将这些权重移动到指定的设备( device ),并乘以类别数( nc )。
    # def labels_to_class_weights(labels, nc=80):
    # -> 它用于根据训练标签计算类别权重。类别权重通常用于在目标检测任务中平衡不同类别的样本数量,使得模型不会偏向于样本数量较多的类别。返回一个 PyTorch 张量,包含每个类别的权重。
    # -> return torch.from_numpy(weights)
    model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc  # attach class weights
    # 附加类别名称。将类别名称( names )附加到模型。
    model.names = names
    # 超参数调整 :这些调整确保超参数与模型结构、数据集和训练设置相匹配。
    # 类别权重 :在目标检测任务中,类别权重可以帮助模型更好地处理类别不平衡问题。
    # 模型参数 :将类别数、超参数和类别名称附加到模型,使得模型可以在训练和推理时使用这些信息。
    # 这段代码展示了如何在PyTorch中设置和调整模型参数,以便在训练目标检测模型时使用。

    # Start training
    # 这段代码是训练过程开始前的初始化和设置部分,包括计时、设置预热迭代次数、初始化结果记录和日志信息等。
    # 开始计时。这行代码记录了训练开始的时间,用于后续计算训练耗时。
    t0 = time.time()
    # 设置预热迭代次数。这行代码计算预热(warmup)阶段的迭代次数,取超参数中指定的预热周期数( hyp['warmup_epochs'] )与批次总数( nb )的乘积,并与1000取最大值,确保至少有1000次迭代作为预热。
    nw = max(round(hyp['warmup_epochs'] * nb), 1000)  # number of warmup iterations, max(3 epochs, 1k iterations)
    # nw = min(nw, (epochs - start_epoch) / 2 * nb)  # limit warmup to < 1/2 of training
    # 初始化mAP记录。这行代码初始化一个数组来记录每个类别的平均精度(mAP)。
    maps = np.zeros(nc)  # mAP per class
    # 初始化结果记录。这行代码初始化一个元组来记录训练结果,包括精确度(P)、召回率(R)、不同IoU阈值下的mAP等。
    results = (0, 0, 0, 0, 0, 0, 0)  # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls)
    # 设置学习率调度器的起始周期。这行代码设置学习率调度器的起始周期,确保从上次训练的周期继续。
    scheduler.last_epoch = start_epoch - 1  # do not move

    # scaler = torch.cuda.amp.GradScaler()
    # torch.cuda.amp.GradScaler 是 PyTorch 中用于自动混合精度(Automatic Mixed Precision, AMP)训练的一个工具,它可以帮助加速模型训练并减少显存使用量。
    # 具体来说, GradScaler 可以将梯度缩放到较小的范围,以避免数值下溢或溢出的问题,同时保持足够的精度以避免模型性能下降。
    # 参数说明 :
    # GradScaler :不需要传递任何参数,直接实例化即可。
    # 主要方法 :
    # scaler.scale(loss) :将损失值缩放,以防止在反向传播时梯度下溢。
    # scaler.step(optimizer) :在反向传播后调用,它首先将梯度值反缩放回来,如果梯度值不是无穷大(inf)或非数值(NaN),则调用优化器的 step() 方法来更新权重,否则忽略 step() 调用,从而保证权重不更新。
    # scaler.update() :在每次迭代后更新 GradScaler 对象的内部状态,动态调整缩放因子,以尽可能减少梯度下溢。
    # 梯度缩放。 GradScaler 通过梯度缩放来防止在半精度(FP16)计算中出现的梯度下溢问题。
    # 动态调整。 GradScaler 动态调整缩放因子,以适应不同的训练阶段和数据。
    # 减少显存占用。 通过使用半精度计算,可以减少显存的占用,允许更大的批量大小和模型尺寸。
    # GradScaler 是 PyTorch 自动混合精度训练中的关键组件,它使得在保持训练精度的同时,能够加速训练过程并优化资源使用。

    # 初始化梯度缩放器。这行代码初始化自动混合精度(AMP)的梯度缩放器,用于在训练中自动调整梯度的缩放,以防止梯度下溢。
    scaler = amp.GradScaler(enabled=cuda)
    # 初始化损失计算类。这两行代码初始化两个损失计算类,用于不同的损失计算方式。

    # 1. ComputeLoss :这是YOLOv7中最基础的损失函数计算类,它通常用于计算传统的YOLO损失,包括物体检测中的分类损失、定位损失(通常是IoU损失)和目标存在损失(对象置信度损失)。
    # ComputeLoss 不包含一些高级的标签分配策略,而是使用更简单的方法来分配正负样本。
    # 2. ComputeLossOTA(Online Throughput Acceleration) :OTA是一种在线吞吐量加速技术,它旨在提高训练的效率和模型的性能。
    # ComputeLossOTA 类中实现了更复杂的正负样本分配策略,例如 find_3_positive 方法,它可能会考虑更多的锚点和网格单元来确定正样本,从而提高模型的检测精度。
    # 这个类还包含一些优化技巧,比如自动平衡不同类别的损失权重,以及使用Focal Loss来处理类别不平衡问题。

    compute_loss_ota = ComputeLossOTA(model)  # init loss class
    compute_loss = ComputeLoss(model)  # init loss class
    # 记录日志信息。这行代码记录了训练的基本信息,包括图像尺寸、数据加载器的工作线程数、结果保存目录和训练周期数。
    logger.info(f'Image sizes {imgsz} train, {imgsz_test} test\n'
                f'Using {dataloader.num_workers} dataloader workers\n'
                f'Logging results to {save_dir}\n'
                f'Starting training for {epochs} epochs...')
    # 保存初始模型。这行代码保存了训练开始前的模型状态,通常用于备份或后续分析。
    # 预热阶段 :预热阶段有助于模型在训练初期更平滑地适应,避免一开始就使用较大的学习率导致训练不稳定。
    # mAP记录 :mAP是目标检测任务中的一个重要指标,记录每个类别的mAP有助于评估模型性能。
    # 梯度缩放器 :在训练中使用梯度缩放器可以提高训练的稳定性和效率。
    # 损失计算类 :自定义损失计算类可以灵活地实现不同的损失函数,适应不同的训练需求。
    # 日志记录 :记录训练的基本信息有助于监控训练过程和后续分析。
    torch.save(model, wdir / 'init.pt')
    for epoch in range(start_epoch, epochs):  # epoch ------------------------------------------------------------------
        # 设置模型为训练模式。这行代码将模型设置为训练模式,这是在每个训练周期开始时的必要步骤。
        model.train()

        # Update image weights (optional)
        # 更新图像权重(如果需要)。这个条件判断检查是否需要根据类别权重和每个类别的mAP来更新图像权重。
        if opt.image_weight
上一篇:CentOS7.9.2009的yum更换vault地窖保险库过期源,epel的archive归档源 笔记241117


下一篇:视频对接rtsp协议学习