当前位置: 首页 > article >正文

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_weights:
            # Generate indices
            if rank in [-1, 0]:
                # 生成类别权重。
                # 如果需要更新图像权重,首先计算类别权重。这里, model.class_weights 是模型的类别权重, maps 是每个类别的mAP, nc 是类别总数。类别权重根据每个类别的mAP进行调整,以给予mAP较低的类别更高的权重。
                cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc  # class weights
                # 计算图像权重。使用 labels_to_image_weights 函数根据数据集的标签和计算出的类别权重来计算图像权重。
                # def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)): -> 它根据类别权重和图像内容生成图像权重。返回图像权重。这行代码返回包含每个图像权重的数组。 -> return image_weights
                # 图像权重更新:在类别不平衡的数据集中,更新图像权重可以帮助模型更好地关注那些mAP较低的类别,从而提高模型的整体性能。
                iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw)  # image weights
                # 根据图像权重随机选择索引。使用 random.choices 函数根据计算出的图像权重随机选择数据集中的索引,以便在训练中给予权重较低的图像更多的机会被选中。
                dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)  # rand weighted idx
            # Broadcast if DDP
            # 在分布式数据并行(DDP)中广播索引。
            if rank != -1:
                # 如果处于分布式训练环境中( rank != -1 ),则需要确保所有进程使用相同的数据索引。
                indices = (torch.tensor(dataset.indices) if rank == 0 else torch.zeros(dataset.n)).int()
                dist.broadcast(indices, 0)
                # 主进程( rank == 0 )将其索引广播给其他进程,其他进程接收这些索引并更新自己的 dataset.indices 。
                if rank != 0:
                    dataset.indices = indices.cpu().numpy()

        # Update mosaic border
        # 更新马赛克边界。
        # 这部分代码被注释掉了,但它的作用是更新数据集中用于马赛克增强的边界值。 imgsz 是图像尺寸, gs 是网格尺寸。 random.uniform 用于生成一个随机数,这个数决定了镶嵌的边界,然后根据网格尺寸 gs 进行调整。
        # dataset.mosaic_border 被设置为 [b - imgsz, -b] ,表示高度和宽度的边界。
        # b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs)
        # dataset.mosaic_border = [b - imgsz, -b]  # height, width borders

        # 初始化损失记录。这行代码初始化一个包含四个元素的零张量 mloss ,用于记录训练过程中的 平均损失 ,包括 边界框损失 、 目标损失 、 类别损失 和 总损失 。
        mloss = torch.zeros(4, device=device)  # mean losses
        # 设置训练进度条。
        # 如果处于分布式训练环境( rank != -1 ),则设置数据加载器的采样器以确保每个epoch的数据分布均匀。
        if rank != -1:
            dataloader.sampler.set_epoch(epoch)
        # enumerate(dataloader) 用于遍历数据加载器。
        pbar = enumerate(dataloader)
        # 用于记录训练的标题信息,包括Epoch、GPU内存使用、各类损失和图像尺寸。
        logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'labels', 'img_size'))
        if rank in [-1, 0]:
            # 如果不是分布式训练环境或处于主进程( rank in [-1, 0] ),则使用 tqdm 创建一个进度条来显示训练进度。
            pbar = tqdm(pbar, total=nb)  # progress bar
        # 清空优化器的梯度。这行代码清空优化器的梯度,这是每次训练迭代开始时的必要步骤,以确保旧的梯度不会影响新的梯度计算。
        # 清空梯度:确保每次迭代的梯度计算都是独立的,避免梯度累积。
        optimizer.zero_grad()
        # 迭代批次。这行代码遍历数据加载器 dataloader 返回的每个批次,其中 imgs 是图像批次 , targets 是对应的目标(标签) , paths 是图像路径 , _ 是一个占位符,表示存在的第四个返回值。
        for i, (imgs, targets, paths, _) in pbar:  # batch -------------------------------------------------------------
            # 计算集成批次数。这行代码计算从训练开始以来的总批次数,包括当前周期和之前周期的批次。
            ni = i + nb * epoch  # number integrated batches (since train start)
            # 图像数据类型转换和归一化。这行代码将图像数据从 uint8 转换为 float32 类型,并将其值从 [0, 255] 归一化到 [0.0, 1.0]。
            imgs = imgs.to(device, non_blocking=True).float() / 255.0  # uint8 to float32, 0-255 to 0.0-1.0

            # Warmup
            # 执行预热策略。
            # 预热策略:预热可以帮助模型在训练初期更平滑地适应,避免一开始就使用较大的学习率导致训练不稳定。
            # 如果集成批次数小于或等于预热迭代次数 nw ,则设置线性插值的边界。
            if ni <= nw:
                xi = [0, nw]  # x interp
                # model.gr = np.interp(ni, xi, [0.0, 1.0])  # iou loss ratio (obj_loss = 1.0 or iou)
                # 调整累积迭代次数。这行代码根据预热阶段的线性插值计算累积迭代次数,以确定何时累积梯度并执行一次优化器步骤。
                accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round())
                # 调整学习率。
                # 学习率和动量调整:在训练过程中动态调整学习率和动量可以帮助模型更好地收敛。
                # 遍历优化器的参数组。这行代码遍历优化器中的所有参数组。 j 是参数组的索引, x 是对应的参数组。
                for j, x in enumerate(optimizer.param_groups):
                    # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
                    # 调整学习率。
                    # np.interp 是 NumPy 的插值函数,用于根据当前的集成批次数 ni 和预热阶段的边界 xi 来计算学习率。
                    # 如果参数组是偏置项(通常索引为2),学习率从 hyp['warmup_bias_lr']  (一个预设的较高初始学习率)开始,逐渐降低到 x['initial_lr'] * lf(epoch) (根据学习率衰减函数 lf 和当前周期 epoch 计算的学习率)。
                    # 对于其他参数组,学习率从0开始,逐渐增加到 x['initial_lr'] * lf(epoch) 。
                    x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
                    # 调整动量。
                    if 'momentum' in x:
                        # 如果参数组包含动量项,使用 np.interp 根据当前的集成批次数 ni 和预热阶段的边界 xi 来计算动量。
                        # 动量从 hyp['warmup_momentum'] (预热阶段的初始动量)开始,逐渐增加到 hyp['momentum'] (正常训练阶段的动量)。
                        x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])
                # 预热阶段的学习率调整 :在训练初期,逐渐增加学习率可以帮助模型更平滑地开始训练,避免因为初始学习率过高而导致的训练不稳定。
                # 偏置项的学习率调整 :偏置项通常需要较高的学习率才能有效更新,因此这段代码特别为偏置项设置了一个从较高值开始逐渐降低的学习率策略。
                # 动量调整 :动量项可以帮助模型加速收敛,并减少震荡,特别是在训练初期。

            # Multi-scale
            # 这段代码处理了在训练深度学习模型时的多尺度(multi-scale)训练策略。多尺度训练是一种数据增强技术,它通过在不同尺度上训练模型来提高模型的泛化能力。
            # 检查是否启用多尺度训练。这行代码检查是否启用了多尺度训练选项。
            if opt.multi_scale:
                # 随机选择图像尺寸。如果启用多尺度训练,这行代码随机选择一个新的图像尺寸 sz ,范围在原始图像尺寸的50%到150%之间,并且是网格尺寸 gs 的整数倍。
                sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs  # size
                # 计算缩放因子。计算缩放因子 sf ,它是新尺寸 sz 与图像最大维度的比值。
                sf = sz / max(imgs.shape[2:])  # scale factor
                # 检查缩放因子是否为1。如果缩放因子不为1,即需要对图像进行缩放。
                if sf != 1:
                    # 计算新的图像形状。计算新的图像形状 ns ,确保新形状是网格尺寸 gs 的整数倍。
                    ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]]  # new shape (stretched to gs-multiple)
                    # 对图像进行插值。使用双线性插值( bilinear )对图像进行缩放,以适应新的形状 ns 。
                    imgs = F.interpolate(imgs, size=ns, mode='bilinear', align_corners=False)
            # 多尺度训练 :通过在不同尺度上训练模型,可以提高模型对不同尺寸输入的适应能力,从而提高模型的泛化性能。
            # 数据增强 :多尺度训练是一种有效的数据增强技术,可以增加训练数据的多样性,有助于模型学习到更加鲁棒的特征。
            # 这段代码展示了如何在训练循环中实现多尺度训练策略。

            # Forward
            # 这段代码是在训练循环中执行前向传播并计算损失的过程。
            # 使用自动混合精度(AMP)。这行代码使用 PyTorch 的自动混合精度(AMP)上下文管理器 autocast 。 enabled=cuda 参数指示仅当使用 CUDA(即 GPU)时才启用自动混合精度,以减少内存消耗并可能加快计算速度。
            with amp.autocast(enabled=cuda):
                # 执行前向传播。这行代码执行模型的前向传播,将图像 imgs 通过模型得到预测结果 pred 。
                pred = model(imgs)  # forward
                # 计算损失。
                # 这行代码调用 compute_loss_ota 函数计算模型预测和目标之间的损失。 pred 是模型的预测结果, targets 是目标标签,通过 .to(device) 确保它们被移动到正确的设备(如 GPU)。 imgs 用于某些损失函数中,需要原始图像数据。
                # 损失值 loss 已经根据批量大小进行了缩放。
                # class ComputeLossOTA:
                # -> def __call__(self, p, targets, imgs):
                # -> 返回 最终的损失值 和 拼接的损失张量 。
                # -> return loss * bs, torch.cat((lbox, lobj, lcls, loss)).detach()
                loss, loss_items = compute_loss_ota(pred, targets.to(device), imgs)  # loss scaled by batch_size
                # 在分布式数据并行(DDP)模式下调整损失。
                # 如果处于分布式训练环境( rank != -1 ),则将损失乘以 world_size (即总设备数),这是因为在 DDP 模式下,梯度会在不同设备间平均。
                if rank != -1:
                    loss *= opt.world_size  # gradient averaged between devices in DDP mode
                # 四倍损失(Quadruplate Loss)。如果启用了四倍损失( opt.quad 为 True ),则将损失乘以 4。这是一种技术,用于在训练初期增加损失函数的权重,以帮助模型更快地收敛。
                if opt.quad:
                    loss *= 4.
            # 自动混合精度 :使用 AMP 可以提高训练速度并减少显存使用,特别是在使用 GPU 训练大型模型时。
            # 损失计算 :计算模型预测和目标之间的损失是训练过程中的关键步骤,它指导了模型参数的更新方向。
            # DDP 模式下的损失调整 :在分布式训练中,确保所有设备上的梯度一致是非常重要的。
            # 四倍损失 :在某些情况下,增加损失的权重可以帮助模型更快地学习。
            # 这段代码展示了如何在训练循环中执行前向传播和损失计算,并根据训练配置调整损失值。

            # Backward
            # 这行代码是在深度学习训练循环中执行反向传播的关键步骤。
            # 梯度缩放。
            # scaler 是一个 torch.cuda.amp.GradScaler 实例,它用于自动混合精度(AMP)训练中梯度的缩放。
            # scale(loss) 方法将计算得到的损失 loss 缩放,以防止在反向传播过程中梯度下溢(即梯度值变得非常小,接近于零)。
            # 反向传播。
            # backward() 方法触发反向传播过程,计算损失 loss 关于模型参数的梯度。
            scaler.scale(loss).backward()
            # 梯度缩放 :在自动混合精度训练中,由于使用了半精度(FP16)计算,梯度值可能会变得非常小,导致数值不稳定。梯度缩放通过放大梯度值来防止这种情况,确保训练的稳定性和效率。
            # 反向传播 :反向传播是深度学习训练中的核心步骤,它根据损失函数计算梯度,这些梯度随后用于更新模型的权重。
            # 这段代码展示了如何在自动混合精度训练中执行反向传播,并通过梯度缩放器来保持训练的稳定性。

            # Optimize
            # 这段代码是在深度学习训练循环中执行优化步骤的部分,包括梯度的更新、梯度缩放器的更新、梯度清零以及EMA(指数移动平均)的更新。
            # 检查是否累积足够的梯度。这行代码检查是否已经累积了足够的梯度。 ni 是自训练开始以来的集成批次数, accumulate 是累积梯度的次数。当 ni 是 accumulate 的倍数时,执行梯度更新。
            if ni % accumulate == 0:
                # 执行优化器步骤。这行代码使用 GradScaler 的 step 方法来执行优化器的 step 方法,更新模型的参数。 scaler.step 会自动处理梯度的缩放和解缩放,确保即使在自动混合精度训练中也能正确更新参数。
                scaler.step(optimizer)  # optimizer.step
                # 更新梯度缩放器。这行代码更新 GradScaler 的内部状态,为下一次迭代准备。
                scaler.update()
                # 清空梯度。这行代码清空优化器中的梯度,为下一次前向传播和反向传播准备。
                optimizer.zero_grad()
                # 更新EMA(指数移动平均)。如果启用了EMA,这行代码更新EMA参数。EMA是一种技术,用于计算模型参数的指数移动平均,这有助于稳定训练过程并提高模型的泛化能力。
                if ema:
                    ema.update(model)
            # 累积梯度 :在某些情况下,累积多个小批量的梯度可以提高训练的稳定性和效率。
            # 梯度更新 :更新模型参数是训练过程中的关键步骤,它使得模型能够学习并改进。
            # 梯度缩放器更新 :更新梯度缩放器的状态,确保在自动混合精度训练中梯度缩放的正确性。
            # EMA更新 :EMA可以提供额外的正则化效果,有助于提高模型的稳定性和性能。
            # 这段代码展示了如何在训练循环中执行优化步骤,并确保在自动混合精度训练中正确更新模型参数。

            # Print
            # 这段代码是在训练循环中用于打印训练进度、更新平均损失、监控内存使用情况,并可选地绘制图像和记录训练过程中的图像。
            if rank in [-1, 0]:
                # 更新平均损失。这行代码更新平均损失 mloss ,其中 i 是当前批次索引, loss_items 是当前批次的损失项。
                # 这个公式是移动平均的更新公式,它结合了之前的移动平均损失和当前批次的损失值,以计算新的移动平均损失。具体来说,新的移动平均损失是之前移动平均损失的加权平均,权重为 i ,加上当前批次损失的权重,权重为1,然后除以总权重 i + 1 。
                # 这种更新方式确保了随着时间的推移,新的损失值对移动平均的影响逐渐增加,而旧的损失值的影响逐渐减小。这有助于减少损失函数的噪声,使得训练过程更加平滑,从而可能提高模型的最终性能。
                mloss = (mloss * i + loss_items) / (i + 1)  # update mean losses
                # 监控内存使用。这行代码计算当前 GPU 保留的内存量(以 GB 为单位),如果 CUDA 不可用,则返回 0。
                mem = '%.3gG' % (torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0)  # (GB)
                # 设置进度条描述。这行代码创建一个字符串 s ,包含当前周期、内存使用情况、平均损失和其他信息,然后使用 pbar.set_description 方法设置进度条的描述。
                s = ('%10s' * 2 + '%10.4g' * 6) % (
                    '%g/%g' % (epoch, epochs - 1), mem, *mloss, targets.shape[0], imgs.shape[-1])
                
                # pbar.set_description(description)
                # pbar.set_description() 是 Python 中 tqdm 库的一个方法,用于设置进度条的描述信息。 tqdm 是一个快速、扩展性强的进度条工具库,可以在 Python 长循环中添加一个进度提示信息。
                # 参数 :
                # description :一个字符串,用来描述进度条当前的状态或者正在进行的任务。
                # 特点和注意事项 :
                # set_description() 方法可以在循环过程中动态更新进度条的描述信息,这对于显示当前任务的状态非常有用。
                # 描述信息会显示在进度条的左侧。
                # 如果你想要在进度条的右侧显示额外的信息,可以使用 set_postfix() 方法。
                # tqdm 会自动处理描述信息的更新,不需要手动刷新屏幕。
                # tqdm 是一个非常灵活的工具,可以通过各种参数和方法来自定义进度条的外观和行为, set_description() 只是其中的一个功能。

                pbar.set_description(s)

                # Plot
                # 绘制图像。
                # 如果启用了绘图选项 plots 并且当前集成批次数 ni 小于 10,则创建一个新线程来绘制当前批次的图像和目标,并保存为 JPEG 文件。
                if plots and ni < 10:
                    f = save_dir / f'train_batch{ni}.jpg'  # filename
                    Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start()
                    # if tb_writer:
                    #     tb_writer.add_image(f, result, dataformats='HWC', global_step=epoch)
                    #     tb_writer.add_graph(torch.jit.trace(model, imgs, strict=False), [])  # add model graph
                # 使用 Weights & Biases 记录图像
                # 如果启用了绘图选项 plots ,当前集成批次数 ni 等于 10,并且有有效的 Weights & Biases 日志记录器,则记录保存在 save_dir 中的训练图像到 Weights & Biases 平台。
                elif plots and ni == 10 and wandb_logger.wandb:
                    wandb_logger.log({"Mosaics": [wandb_logger.wandb.Image(str(x), caption=x.name) for x in
                                                  save_dir.glob('train*.jpg') if x.exists()]})
            # 训练进度监控 :通过打印训练进度和损失,可以帮助开发者实时了解训练状态。
            # 内存使用监控 :监控 GPU 内存使用情况,避免内存溢出。
            # 图像绘制 :绘制训练过程中的图像可以帮助可视化模型的预测结果,以及目标和实际之间的差异。
            # 使用 Weights & Biases:记录训练过程中的图像到 Weights & Biases 平台,可以方便地进行模型比较和结果分享。
            # 这段代码展示了如何在训练循环中打印训练进度、监控内存使用情况,并可选地绘制和记录训练过程中的图像。

            # end batch ------------------------------------------------------------------------------------------------
        # end epoch ----------------------------------------------------------------------------------------------------

        # Scheduler
        # 这段代码涉及到学习率调度器(scheduler)的使用,特别是在深度学习训练过程中更新学习率。
        # 收集当前学习率。这行代码遍历优化器的所有参数组,收集每个参数组当前的学习率,并将它们存储在列表 lr 中。这个列表可以用于记录或可视化学习率的变化,例如在TensorBoard中展示。
        lr = [x['lr'] for x in optimizer.param_groups]  # for tensorboard
        # 更新学习率调度器。这行代码调用学习率调度器的 step 方法,根据预设的策略更新学习率。学习率调度器根据当前的训练周期(epoch)和批次(batch)来调整学习率,以优化训练过程。
        scheduler.step()
        # 学习率调整 :在训练过程中动态调整学习率是非常重要的,因为它可以帮助模型更快地收敛,并避免在训练初期由于学习率过高而导致的不稳定,或在训练后期由于学习率过低而导致的收敛速度过慢。
        # 记录学习率 :记录每个参数组的学习率对于监控训练过程和调试模型非常有帮助,特别是在使用不同的学习率策略时。
        # 这段代码展示了如何在训练循环中使用学习率调度器,并收集当前的学习率信息。

        # DDP process 0 or single-GPU
        # 接下来的代码块适用于分布式数据并行(DDP)中的第一个进程(通常是rank为0的进程)或者在单GPU训练环境中。
        # 这个条件判断检查当前进程的 rank 值。 rank 为-1通常表示单GPU训练环境,而rank为0表示分布式训练中的第一个进程。
        if rank in [-1, 0]:
            # mAP
            # 这行代码调用 ema (指数移动平均)对象的  update_attr  方法,更新模型的属性。
            # include 参数指定了需要更新的属性列表,这些属性可能包括模型配置( yaml )、类别数( nc )、超参数( hyp )、网格尺寸( gr )、类别名称( names )、步长( stride )和类别权重( class_weights )。
            # class ModelEMA:
            # -> def update_attr(self, model, include=(), exclude=('process_group', 'reducer')):
            # -> 它用于将指数移动平均(EMA)模型的属性更新到原始模型。
            ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'gr', 'names', 'stride', 'class_weights'])
            # 这行代码设置一个布尔变量 final_epoch ,用于标记当前是否是训练的最后一个周期(epoch)。如果是,则 final_epoch 为True。
            final_epoch = epoch + 1 == epochs
            # 这个条件判断检查是否不应该进行测试( opt.notest )或者是否是最后一个训练周期。如果用户没有设置 notest 选项(即用户想要进行测试)或者当前是最后一个训练周期,则执行后面的代码块。
            if not opt.notest or final_epoch:  # Calculate mAP
                # 更新Weights & Biases(wandb)日志记录器中的当前周期。
                wandb_logger.current_epoch = epoch + 1
                # 这行代码调用 test 模块的 test 函数来执行模型的评估,并计算结果、平均精度(mAP)和测试时间。函数参数包括 :
                # data_dict :包含测试数据集的相关信息。
                # batch_size :测试时的批量大小,这里乘以2可能是为了加快测试速度。
                # imgsz :测试图像的尺寸。
                # model :用于测试的模型,这里使用的是经过EMA处理的模型( ema.ema )。
                # single_cls :是否为单类别检测。
                # dataloader :测试数据的加载器。
                # save_dir :保存测试结果的目录。
                # verbose :是否打印详细信息,这里条件是类别数少于50且是最后一个周期。
                # plots :是否生成图表,这里条件是生成图表且是最后一个周期。
                # wandb_logger :用于记录wandb日志的logger对象。
                # compute_loss :是否计算损失。
                # is_coco :是否使用COCO数据集
                results, maps, times = test.test(data_dict,
                                                 batch_size=batch_size * 2,
                                                 imgsz=imgsz_test,
                                                 model=ema.ema,
                                                 single_cls=opt.single_cls,
                                                 dataloader=testloader,
                                                 save_dir=save_dir,
                                                 verbose=nc < 50 and final_epoch,
                                                 plots=plots and final_epoch,
                                                 wandb_logger=wandb_logger,
                                                 compute_loss=compute_loss,
                                                 is_coco=is_coco)
            # 这段代码的主要目的是在训练过程中的特定条件下(如每个周期结束时或仅在最后一个周期结束时)对模型进行评估,以监控模型性能。使用wandb记录实验进度和结果,以及可能的可视化输出。

            # Write
            # 这段代码是用于将模型评估结果写入本地文件,并在满足特定条件时将这些结果上传到Google Cloud Storage(GCS)的脚本。
            # 这行代码使用 with 语句打开一个文件,文件名为 results_file 变量指定的路径,以追加模式('a')打开。这意味着如果文件已存在,新内容将被添加到文件末尾。
            with open(results_file, 'a') as f:
                # 这行代码将评估结果写入文件。
                # s 是一个字符串,包含一些前缀信息。 '%10.4g' * 7 是一个格式化字符串,用于格式化7个浮点数,每个数占用10个字符宽度,其中小数点后保留4位。 results 是一个包含评估结果的元组或列表,其元素将被格式化并写入文件。 \n 表示新行,确保每个结果占一行。
                f.write(s + '%10.4g' * 7 % results + '\n')  # append metrics, val_loss
            # 这个条件判断检查 opt.name 是否有内容(即非空)且 opt.bucket 是否存在。这两个变量通常用于指定GCS的存储桶名称和文件名前缀。
            if len(opt.name) and opt.bucket:
                # 如果条件满足,这行代码使用 os.system 函数调用系统命令,将本地的评估结果文件 results_file 上传到GCS。
                # gsutil 是Google Cloud Storage的命令行工具,用于管理GCS资源。 cp 命令用于复制文件。 gs://%s/results/results%s.txt 是GCS上的路径,其中 %s 会被 opt.bucket 和 opt.name 替换,形成完整的文件路径。
                os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name))
            # 这段代码的目的是将模型的评估结果保存到本地文件,并在满足特定条件时(即当提供了存储桶名称和文件名前缀时)将这些结果上传到云端存储,以便进行远程访问和备份。这是一种常见的做法,用于在分布式训练和大规模机器学习项目中跟踪和共享模型性能指标。

            # Log
            # 这段代码是用于记录训练和验证过程中的关键指标到TensorBoard和Weights & Biases(W&B)的日志系统中。
            # 这行代码定义了一个名为 tags 的列表,其中包含了所有需要记录的指标的标签。
            # 这些标签分为三类 :
            # 训练损失( train/box_loss , train/obj_loss , train/cls_loss )。
            # 评估指标( metrics/precision , metrics/recall , metrics/mAP_0.5 , metrics/mAP_0.5:0.95 )。
            # 验证损失( val/box_loss , val/obj_loss , val/cls_loss )。
            # 参数( x/lr0 , x/lr1 , x/lr2 )。
            tags = ['train/box_loss', 'train/obj_loss', 'train/cls_loss',  # train loss
                    'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95',
                    'val/box_loss', 'val/obj_loss', 'val/cls_loss',  # val loss
                    'x/lr0', 'x/lr1', 'x/lr2']  # params
            # 这行代码使用 zip 函数将三个列表( mloss 的前部分, results ,和 lr )与 tags 列表中的标签配对。
            # mloss[:-1] 表示 mloss 列表除了最后一个元素之外的所有元素。
            # list(results) 将 results 转换为列表(如果它还不是列表)。
            # lr 是一个包含学习率值的列表。这个循环将遍历所有的指标和它们的标签。
            for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags):
                # 这个条件判断检查是否存在 tb_writer 对象。 tb_writer 是TensorBoard的写入器对象。
                if tb_writer:
                    # 如果 tb_writer 存在,这行代码使用 add_scalar 方法将指标 x 和它的标签 tag 记录到TensorBoard中,并指定当前的周期 epoch 。
                    tb_writer.add_scalar(tag, x, epoch)  # tensorboard
                # 这个条件判断检查 wandb_logger 对象中是否有 wandb 属性。
                if wandb_logger.wandb:
                    # 如果 wandb_logger.wandb 存在,这行代码使用 log 方法将指标 x 和它的标签 tag 记录到Weights & Biases中。
                    wandb_logger.log({tag: x})  # W&B
            # 这段代码的主要目的是在训练和验证过程中,将关键的性能指标和参数记录到TensorBoard和W&B中,以便进行可视化和监控。
            # TensorBoard是一个强大的工具,用于可视化训练过程,而W&B提供了一个平台来跟踪实验、记录模型性能和参数,并与团队成员共享结果。
            # 通过这种方式,研究人员和开发人员可以更好地理解模型的行为,并优化训练过程。

            # Update best mAP
            # 这段代码是用于更新模型的最佳平均精度(mAP)并在训练过程中记录这一指标。
            # 这行代码调用一个名为 fitness 的函数,该函数接受一个数组作为输入,并返回一个加权组合的适应度分数。
            # np.array(results) 将 results 转换为NumPy数组, reshape(1, -1) 将数组重塑为1行N列的形式,其中N是 results 中的元素数量。这个适应度分数是基于精确度(P)、召回率(R)和不同阈值下的mAP值的加权组合。 [P, R, mAP@0.5, mAP@0.5:0.95]
            # def fitness(x): -> 用于计算模型的适应度(fitness),作为一个加权组合的度量指标。 -> return (x[:, :4] * w).sum(1)
            fi = fitness(np.array(results).reshape(1, -1))  # weighted combination of [P, R, mAP@.5, mAP@.5-.95]
            # 这个条件判断检查新计算的适应度分数 fi 是否大于当前记录的最佳适应度分数 best_fitness 。
            if fi > best_fitness:
                # 如果新计算的适应度分数 fi 大于 best_fitness ,则更新 best_fitness 为 fi 的值。
                best_fitness = fi
            # 这行代码调用 wandb_logger 的 end_epoch 方法来标记一个训练周期的结束,并传递一个参数 best_result 。
            # best_result 是一个布尔值,指示当前周期的适应度分数是否是最佳分数。如果 best_fitness 被更新为 fi ,则 best_fitness == fi 为True,表示当前周期是最佳结果。
            wandb_logger.end_epoch(best_result=best_fitness == fi)
            # 这段代码的主要目的是在训练过程中监控模型的性能,并记录下最佳的mAP分数。通过这种方式,研究人员和开发人员可以跟踪模型的最佳性能,并在训练结束后快速识别出最佳的模型检查点。
            # Weights & Biases(W&B)作为一个实验跟踪工具,可以帮助用户可视化和比较不同训练周期的结果,以及保存最佳模型的配置和性能指标。

            # Save model
            # 这段代码是用于保存模型检查点(checkpoint)的完整脚本,包括保存最后的状态、最佳模型以及在特定条件下的额外保存。
            # 这个条件判断检查是否应该保存模型。如果用户没有设置 nosave 选项(即用户想要保存模型),或者当前是最后一个训练周期且用户没有设置 evolve 选项(即用户不打算进一步进化模型),则执行保存操作。
            if (not opt.nosave) or (final_epoch and not opt.evolve):  # if save
                # 初始化一个字典 ckpt ,用于存储检查点的所有相关信息,包括当前周期、最佳适应度分数、训练结果、模型权重、EMA权重、优化器状态等。
                ckpt = {'epoch': epoch,
                        'best_fitness': best_fitness,
                        'training_results': results_file.read_text(),
                        'model': deepcopy(model.module if is_parallel(model) else model).half(),
                        'ema': deepcopy(ema.ema).half(),
                        'updates': ema.updates,
                        'optimizer': optimizer.state_dict(),
                        'wandb_id': wandb_logger.wandb_run.id if wandb_logger.wandb else None}

                # Save last, best and delete
                # 使用PyTorch的 torch.save 函数将检查点字典保存到文件 last 中,这个文件通常用于存储最新的模型状态。
                torch.save(ckpt, last)
                # 如果当前的适应度分数 fi 等于最佳适应度分数 best_fitness ,则执行以下操作。
                if best_fitness == fi:
                    # 将检查点字典保存到文件 best 中,这个文件通常用于存储最佳模型状态。
                    torch.save(ckpt, best)
                # 如果当前的适应度分数 fi 等于最佳适应度分数 best_fitness 且当前周期大于或等于200,则执行以下操作。
                if (best_fitness == fi) and (epoch >= 200):
                    # 将检查点字典保存到一个以周期编号命名的文件中,文件名格式为 best_{epoch}.pt 。
                    torch.save(ckpt, wdir / 'best_{:03d}.pt'.format(epoch))
                # if epoch == 0: 或 elif ((epoch+1) % 25) == 0: 或 elif epoch >= (epochs-5): : 在特定的周期(如第一个周期、每25个周期、最后5个周期)保存检查点。
                if epoch == 0:
                    torch.save(ckpt, wdir / 'epoch_{:03d}.pt'.format(epoch))
                elif ((epoch+1) % 25) == 0:
                    torch.save(ckpt, wdir / 'epoch_{:03d}.pt'.format(epoch))
                elif epoch >= (epochs-5):
                    torch.save(ckpt, wdir / 'epoch_{:03d}.pt'.format(epoch))
                # 如果使用Weights & Biases,则执行以下操作。
                if wandb_logger.wandb:
                    # (epoch + 1) % opt.save_period == 0 : 这个表达式检查当前周期数加一( epoch + 1 )除以用户指定的保存周期( opt.save_period )的余数是否为零。如果余数为零,这意味着当前周期数是保存周期的整数倍,即到了用户设定的保存模型的时间点。
                    # opt.save_period != -1 : 这个表达式检查用户指定的保存周期( opt.save_period )是否不等于-1。如果 opt.save_period 被设置为-1,通常意味着用户不希望按照周期自动保存模型。
                    # 这个条件语句结合了三个条件。它只在以下情况下为真 :
                    # 当前周期数是保存周期的整数倍(即到了保存模型的时间点)。
                    # 当前周期不是最后一个训练周期。
                    # 用户指定的保存周期不是-1(即用户希望按照周期自动保存模型)。
                    # 这个条件语句确保了模型检查点会在用户指定的周期间隔保存,但不会在训练的最后一个周期保存(除非 final_epoch 为False),也不会在 opt.save_period 被设置为-1时保存。
                    if ((epoch + 1) % opt.save_period == 0 and not final_epoch) and opt.save_period != -1:
                        # 使用W&B的 log_model 方法记录模型。这个方法会将模型文件上传到W&B,并记录相关的训练参数和性能指标。
                        wandb_logger.log_model(
                            last.parent, opt, epoch, fi, best_model=best_fitness == fi)
                # 删除 ckpt 字典,以释放内存。
                del ckpt
            # 这段代码的主要目的是在训练过程中保存模型的状态,包括当前周期、最佳适应度分数、训练结果、模型权重、EMA权重、优化器状态等。这些信息可以在以后用于恢复训练、进行模型评估或部署模型。
            # 通过在特定条件下保存模型,可以确保即使训练过程中断,也能从最近的检查点恢复,同时保留最佳模型和关键周期的模型状态。使用Weights & Biases可以方便地跟踪和共享模型文件。

        # end epoch ----------------------------------------------------------------------------------------------------
    # end training
    # 在一个深度学习训练脚本中,用于在特定条件下(如分布式训练的主进程或单GPU训练环境)执行绘图和测试最佳模型的性能。
    # 这个条件判断检查当前进程的 rank 值。 rank 为-1通常表示单GPU训练环境,而rank为0表示分布式训练中的第一个进程。
    if rank in [-1, 0]:
        # Plots
        # 这个条件判断检查 plots 变量是否为True,如果是,则执行绘图操作。
        if plots:
            # 调用 plot_results 函数生成绘图结果,并保存为 results.png 。
            # def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): -> 用于绘制训练过程中的 'results*.txt' 文件中记录的指标。
            plot_results(save_dir=save_dir)  # save as results.png
            # 如果使用Weights & Biases(W&B),则执行以下操作。
            if wandb_logger.wandb:
                # 创建一个包含结果文件名的列表,包括结果图、混淆矩阵图以及F1、PR、P、R曲线图。
                files = ['results.png', 'confusion_matrix.png', *[f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R')]]
                # 使用W&B的 log 方法记录图像文件。这里使用了列表推导式来创建一个W&B图像对象的列表,每个对象对应一个文件,并将这些图像对象作为结果日志记录。
                wandb_logger.log({"Results": [wandb_logger.wandb.Image(str(save_dir / f), caption=f) for f in files
                                              if (save_dir / f).exists()]})
        # Test best.pt
        # 接下来的代码块用于测试最佳模型的性能。
        # 使用日志记录器记录完成的周期数和训练耗时(小时)。
        logger.info('%g epochs completed in %.3f hours.\n' % (epoch - start_epoch + 1, (time.time() - t0) / 3600))
        # 这个条件判断检查数据配置文件是否以 coco.yaml 结尾且类别数 nc 是否为80,这通常用于COCO数据集。
        if opt.data.endswith('coco.yaml') and nc == 80:  # if COCO
            # 这个循环遍历最后保存的模型和最佳模型(如果最佳模型存在),否则只遍历最后保存的模型。
            for m in (last, best) if best.exists() else (last):  # speed, mAP tests
                # 调用 test.test 函数测试模型性能,传入测试数据配置、批量大小、图像尺寸、置信度阈值、IoU阈值、模型、单类别检测标志、测试数据加载器、保存目录、保存JSON结果标志、绘图标志和是否为COCO数据集标志。
                results, _, _ = test.test(opt.data,
                                          batch_size=batch_size * 2,
                                          imgsz=imgsz_test,
                                          conf_thres=0.001,
                                          iou_thres=0.7,
                                          model=attempt_load(m, device).half(),
                                          single_cls=opt.single_cls,
                                          dataloader=testloader,
                                          save_dir=save_dir,
                                          save_json=True,
                                          plots=False,
                                          is_coco=is_coco)

        # Strip optimizers
        # 接下来的代码块用于去除模型检查点中的优化器信息。
        # 这行代码确定最终保存的模型文件是最佳模型( best )如果它存在,否则是最后保存的模型( last )。
        final = best if best.exists() else last  # final model
        # 这个循环遍历最后保存的模型和最佳模型。
        for f in last, best:
            # 这个条件判断检查模型文件是否存在。
            if f.exists():
                # 如果模型文件存在,调用 strip_optimizer 函数去除模型检查点中的优化器信息。这通常是为了减小模型文件的大小,因为优化器状态通常不需要用于模型部署。
                # def strip_optimizer(f='best.pt', s=''):  -> 从训练好的模型文件中移除优化器(optimizer)和其他非必要的信息,以便减小模型文件的大小,使其更适合部署。这个函数还允许你将处理后的模型保存为一个新的文件。
                strip_optimizer(f)  # strip optimizers
        # 这个条件判断检查是否有指定的云端存储桶( opt.bucket )。
        if opt.bucket:
            # 如果有指定的存储桶,使用 os.system 调用系统命令,将最终的模型文件上传到Google Cloud Storage(GCS)。这里使用的是 gsutil 命令行工具。
            os.system(f'gsutil cp {final} gs://{opt.bucket}/weights')  # upload
        # 这个条件判断检查是否使用W&B( wandb_logger.wandb )且用户没有设置 evolve 选项。
        if wandb_logger.wandb and not opt.evolve:  # Log the stripped model
            # 如果满足条件,使用W&B的 log_artifact 方法记录模型文件。这个方法会将模型文件上传到W&B,并记录相关的信息,如运行ID、模型类型和别名。
            wandb_logger.wandb.log_artifact(str(final), type='model',
                                            name='run_' + wandb_logger.wandb_run.id + '_model',
                                            aliases=['last', 'best', 'stripped'])
        # 最后,调用 wandb_logger.finish_run 方法结束W&B的运行记录。这会标记实验的结束,并保存所有记录的数据。
        wandb_logger.finish_run()
    else:
        # 这行代码调用 dist (分布式通信包,通常是PyTorch的 torch.distributed )的 destroy_process_group 方法来销毁进程组。
        # 在分布式训练中,每个进程都会加入一个进程组以进行通信。当训练结束后,需要销毁这个进程组以释放资源。这通常在所有训练任务完成后由非零排名的进程调用。
        dist.destroy_process_group()
    
    # torch.cuda.empty_cache()
    # torch.cuda.empty_cache() 是 PyTorch 提供的一个函数,用于清空当前 GPU 中的缓存内存。这个函数可以帮助释放那些由 PyTorch 管理的、已经不再使用的缓存内存,但是需要注意的是,它并不会释放由 Python 垃圾回收器管理的内存。
    # 参数 :无参数。
    # 返回值 :无返回值。
    # 特点和注意事项 :
    # torch.cuda.empty_cache() 会立即返回,而不会等待内存实际被操作系统回收。
    # 这个函数只影响 PyTorch 的缓存机制,不会影响其他由 PyTorch 分配的内存,也不会影响 Python 进程中的其他内存分配。
    # 在大多数情况下,PyTorch 的内存管理是自动的,不需要手动调用 empty_cache() 。但在长时间运行的程序中,或者在内存使用紧张的情况下,合理地使用 empty_cache() 可以帮助减少内存碎片和潜在的内存不足问题。
    # 过度频繁地调用 empty_cache() 可能会导致性能下降,因为它会打断 PyTorch 的内存分配策略,所以应当谨慎使用。
    # 总的来说, torch.cuda.empty_cache() 是一个用于管理 GPU 内存的工具,但在实际使用中,通常不需要手动干预 PyTorch 的内存管理机制。

    # 这行代码调用PyTorch的 torch.cuda.empty_cache 函数来清空CUDA缓存。在深度学习训练过程中,CUDA缓存用于存储中间结果以加快计算速度。清空缓存可以释放不再需要的GPU内存,这对于管理GPU资源和避免内存泄漏是很重要的。
    torch.cuda.empty_cache()
    # 这行代码返回训练或评估过程的结果。 results 变量包含了训练过程中的一些关键指标,如损失值、精确度、召回率等,这些信息对于分析模型性能和调整超参数很有用。
    return results

3.if __name__ == '__main__': 

# if __name__ == '__main__' : 是一个常用的模式,用于判断当前脚本是否作为主程序运行。如果是,那么以下代码块将被执行;如果不是(即该模块被其他 Python 脚本导入),则不执行。
if __name__ == '__main__':
    # argparse.ArgumentParser() 是 Python 标准库 argparse 模块中的一个函数,用于创建命令行参数解析器。这个解析器可以定义需要哪些命令行参数,每个参数的类型、默认值、帮助信息等。
    parser = argparse.ArgumentParser()
    # --weights : 指定模型的初始权重文件路径,默认为 'yolo7.pt'。
    parser.add_argument('--weights', type=str, default='yolo7.pt', help='initial weights path')
    # --cfg : 指定模型配置文件(如 YAML 文件)的路径,默认为空。
    parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
    # --data : 指定数据集配置文件的路径,默认为 'data/coco.yaml'。
    parser.add_argument('--data', type=str, default='data/coco.yaml', help='data.yaml path')
    # --hyp : 指定超参数配置文件的路径,默认为 'data/hyp.scratch.p5.yaml'。
    parser.add_argument('--hyp', type=str, default='data/hyp.scratch.p5.yaml', help='hyperparameters path')
    # --epochs : 指定训练的总轮数,默认为 300。
    parser.add_argument('--epochs', type=int, default=300)
    # --batch-size : 指定所有 GPU 上的总批次大小,默认为 16。
    parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
    # --img-size : 指定训练和测试时的图像尺寸,默认为 [640, 640]。
    parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes')
    # --rect : 如果设置,将使用矩形训练。
    parser.add_argument('--rect', action='store_true', help='rectangular training')
    # --resume : 如果设置,将恢复最近的训练。
    parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
    # --nosave : 如果设置,仅保存最终的检查点。
    parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
    # --notest : 如果设置,仅在最后一个 epoch 进行测试。
    parser.add_argument('--notest', action='store_true', help='only test final epoch')
    # --noautoanchor : 如果设置,将禁用自动锚点检查。
    parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
    # --evolve : 如果设置,将进化超参数。
    parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters')
    # --bucket : 指定用于存储的 gsutil 桶。
    parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
    # --cache-images : 如果设置,将缓存图像以加快训练速度。
    parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
    # --image-weights : 如果设置,将使用加权图像选择进行训练。
    parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
    # --device : 指定使用的 CUDA 设备,例如 '0' 或 '0,1,2,3',或者 'cpu'。
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    # --multi-scale : 如果设置,将变化图像尺寸 +/- 50%。
    parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
    # --single-cls : 如果设置,将把多类数据当作单类数据进行训练。
    parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
    # --adam : 如果设置,将使用 Adam 优化器。
    parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
    # --sync-bn : 如果设置,将使用同步批量归一化,仅在 DDP 模式下可用。
    parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
    # --local_rank : DDP 参数,不要修改,默认为 -1。
    parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
    # --workers : 指定数据加载器工作线程的最大数量,默认为 8。
    parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
    # --project : 指定保存项目的路径,默认为 'runs/train'。
    parser.add_argument('--project', default='runs/train', help='save to project/name')
    # --entity : 指定 W&B 实体。
    parser.add_argument('--entity', default=None, help='W&B entity')
    # --name : 指定保存的项目名称,默认为 'exp'。
    parser.add_argument('--name', default='exp', help='save to project/name')
    # --exist-ok : 如果设置,允许使用已存在的项目/名称,不进行增量。
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    # --quad : 如果设置,将使用四倍数据加载器。
    parser.add_argument('--quad', action='store_true', help='quad dataloader')
    # --linear-lr : 如果设置,将使用线性学习率。
    parser.add_argument('--linear-lr', action='store_true', help='linear LR')
    # --label-smoothing : 指定标签平滑的 epsilon 值,默认为 0.0。
    parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
    # --upload_dataset : 如果设置,将数据集上传为 W&B 工件表。
    parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table')
    # --bbox_interval : 指定 W&B 的边界框图像记录间隔,默认为 -1。
    parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B')
    # --save_period : 指定每 'save_period' 个 epoch 记录模型,默认为 -1。
    parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch')
    # --artifact_alias : 指定要使用的工件数据集的版本,默认为 "latest"。
    parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used')
    # parser :这是之前创建的 ArgumentParser 对象,它已经通过 add_argument() 方法定义了所有需要的命令行参数。
    # parse_args() :这个方法会读取命令行输入的参数,并将它们转换为 Python 可以理解的数据类型(例如,字符串、整数、布尔值等)。
    # opt :这是一个变量,用来存储 parse_args() 方法返回的命名空间对象。
    # 这个对象包含了所有解析后的命令行参数值,可以通过属性的方式访问,例如 opt.epochs 、 opt.batch_size 等。
    opt = parser.parse_args()

    # Set DDP variables
    # 这段代码是在设置分布式数据并行(Distributed Data Parallel,简称 DDP)相关的变量。DDP 是 PyTorch 中用于加速训练的一种方式,它允许模型在多个 GPU 上并行训练。
    # 这个变量表示参与分布式训练的总进程数(或 GPU 数)。
    # 代码从环境变量 WORLD_SIZE 中获取这个值,如果环境变量中没有 WORLD_SIZE ,则默认设置为 1 。这意味着如果没有明确指定,就假定只有一个进程参与训练。
    opt.world_size = int(os.environ['WORLD_SIZE']) if 'WORLD_SIZE' in os.environ else 1
    # 这个变量表示当前进程在所有进程中的全局索引或排名。
    # 代码从环境变量 RANK 中获取这个值,如果环境变量中没有 RANK ,则默认设置为 -1 。 -1 通常表示当前进程不是分布式训练的一部分,或者是一个非分布式训练的进程。
    opt.global_rank = int(os.environ['RANK']) if 'RANK' in os.environ else -1
    # 这个函数调用是为了设置日志记录,以便根据全局排名来区分不同进程的日志输出。
    # def set_logging(rank=-1): -> 它用于配置 Python 的日志记录系统。这个 set_logging 函数的目的是在程序开始时设置合适的日志记录级别。在分布式训练环境中, rank 参数可以用来区分主进程和其他进程,以便在非主进程中减少日志输出,避免日志信息的冗余。
    set_logging(opt.global_rank)
    # 这是一个被注释掉的条件判断,如果 global_rank 是 -1 或 0 ,则执行以下两个函数调用。
    #if opt.global_rank in [-1, 0]:
    # 这个函数调用是用来检查当前的 Git 仓库状态,确保代码是最新的,或者没有未提交的更改,这对于确保分布式训练的一致性很重要。
    #    check_git_status()
    # 这个函数调用是用来检查环境是否满足训练所需的依赖,比如确保所有必要的库都已安装。
    #    check_requirements()

    # Resume
    # 这段代码处理的是训练过程中的恢复逻辑,以及一些与训练配置相关的设置。
    # 这行代码调用 check_wandb_resume 函数 ,用于检查是否需要从 Weights & Biases (W&B) 平台恢复训练。
    wandb_run = check_wandb_resume(opt)
    # 如果 opt.resume 为真(即用户希望恢复训练),且没有从 W&B 平台恢复训练,则执行以下逻辑。
    if opt.resume and not wandb_run:  # resume an interrupted run
        # 如果 opt.resume 是一个字符串,那么 ckpt 就是这个字符串指定的路径;否则,调用 get_latest_run 函数来获取最近的运行路径。
        # def get_latest_run(search_dir='.'):
        # -> 在指定的目录(默认为当前目录)及其子目录中查找最新的 last.pt 文件。如果 last_list 不为空,使用 max 函数找到列表中最“新”的文件。这里的“新”是基于文件的创建时间( os.path.getctime ),即返回创建时间最晚的文件路径。
        # -> return max(last_list, key=os.path.getctime) if last_list else ''
        ckpt = opt.resume if isinstance(opt.resume, str) else get_latest_run()  # specified or most recent path
        # 确保恢复路径 ckpt 指向的文件存在,否则抛出错误。
        assert os.path.isfile(ckpt), 'ERROR: --resume checkpoint does not exist'    # 错误:--恢复检查点不存在。
        # 保存当前的全局排名和本地排名,以便之后恢复。
        apriori = opt.global_rank, opt.local_rank
        # 打开 ckpt 路径的父目录的父目录下的 opt.yaml 文件,这个文件包含了之前训练的配置。
        # 使用 yaml.load 加载配置,并更新 opt 对象。
        with open(Path(ckpt).parent.parent / 'opt.yaml') as f:
            opt = argparse.Namespace(**yaml.load(f, Loader=yaml.SafeLoader))  # replace
        # 重新设置 opt 对象的某些属性,以确保恢复训练时使用正确的配置。
        opt.cfg, opt.weights, opt.resume, opt.batch_size, opt.global_rank, opt.local_rank = '', ckpt, True, opt.total_batch_size, *apriori  # reinstate
        # 记录一条信息,表明从哪个检查点恢复训练。
        logger.info('Resuming training from %s' % ckpt)
    # 如果不需要恢复训练,则执行以下逻辑。
    else:
        # opt.hyp = opt.hyp or ('hyp.finetune.yaml' if opt.weights else 'hyp.scratch.yaml')
        # 调用 check_file 函数来验证数据、配置和超参数文件是否存在。
        # def check_file(file): -> 在给定文件路径不存在时,在当前目录及其子目录中搜索文件。如果找到了文件,函数将返回文件的路径;如果未找到或有多个匹配的文件,函数将抛出异常。返回文件路径。如果文件存在且唯一,返回找到的文件路径。 -> return files[0]
        opt.data, opt.cfg, opt.hyp = check_file(opt.data), check_file(opt.cfg), check_file(opt.hyp)  # check files
        # 确保至少指定了配置文件或权重文件。
        assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified'    # 必须指定 --cfg 或 --weights。

        # list.extend(iterable)
        # 在 Python 中, .extend() 方法是列表(list)对象的一个方法,用于将一个可迭代对象(如列表、元组、字符串等)的所有元素添加到列表的末尾。
        # list : 需要扩展的列表对象。
        # iterable : 一个可迭代对象,其所有元素将被添加到列表中。
        # 返回值 :
        # .extend() 方法没有返回值(即返回 None ),因为它直接修改列表对象本身。

        # 确保 opt.img_size 列表至少有两个元素(训练和测试图像尺寸)。
        opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size)))  # extend to 2 sizes (train, test)
        # 如果 opt.evolve 为真,则将实验名称设置为 'evolve' 。
        opt.name = 'evolve' if opt.evolve else opt.name
        # 调用 increment_path 函数来生成一个唯一的保存目录,如果目录已存在,则增加后缀以避免覆盖。
        # def increment_path(path, exist_ok=True, sep=''): -> 用于生成一个唯一的文件路径。如果指定的路径已经存在,函数会通过添加一个数字后缀来创建一个新的路径。返回一个新的路径,它是原始路径加上一个更新的数字后缀。 -> return f"{path}{sep}{n}"  # update path
        opt.save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok | opt.evolve)  # increment run
    # 这段代码的目的是确保可以从中断的地方恢复训练,并且正确地设置训练配置。如果不需要恢复训练,则验证文件的存在性,并设置实验的保存目录。

    # DDP mode
    # 这段代码是用于设置分布式数据并行(Distributed Data Parallel,简称 DDP)模式的配置。
    # 将 opt.total_batch_size 设置为与 opt.batch_size 相同。 total_batch_size 表示所有 GPU 上的总批次大小,而 batch_size 表示每个 GPU 上的批次大小。
    opt.total_batch_size = opt.batch_size
    # 调用 select_device 函数来选择使用的设备(GPU或CPU),并传入批次大小作为参数。
    device = select_device(opt.device, batch_size=opt.batch_size)
    # 如果 opt.local_rank 不等于 -1 ,则表示当前环境支持分布式训练。
    if opt.local_rank != -1:

        # torch.cuda.device_count() 
        # torch.cuda.device_count() 是 PyTorch 提供的一个函数,用于返回当前系统中可用的 CUDA 设备(即 NVIDIA GPU)的数量。这个函数不需要任何参数,它会返回一个整数,表示系统中可以被 PyTorch 识别和使用的 GPU 数量。
        # 以下是 torch.cuda.device_count() 函数的一些关键点 :
        # 环境要求 :你的系统必须安装了 NVIDIA 的 CUDA 驱动,并且 PyTorch 版本需要支持 CUDA。
        # 返回值 :返回的是一个整数,表示可用的 GPU 数量。如果没有可用的 CUDA 设备,函数将返回 0。
        # 用途 :这个函数常用于分布式训练或者多 GPU 训练的场景,用于动态确定系统中的 GPU 数量,以便合理分配任务或者调整训练配置。

        # 确保可用的 CUDA 设备数量大于 opt.local_rank ,即当前进程的局部索引。
        assert torch.cuda.device_count() > opt.local_rank

        # torch.cuda.set_device(device)
        # torch.cuda.set_device 是 PyTorch 中用于设置当前 CUDA 设备(即 NVIDIA GPU)的函数。这个函数允许你指定一个特定的 GPU 来运行你的 PyTorch 代码。在多 GPU 环境中,这个函数非常有用,因为它可以让你控制代码在哪个 GPU 上执行。
        # 参数 :
        # device :一个整数或字符串,指定要设置的 CUDA 设备的索引或名称。索引从 0 开始。
        # 功能和用途 :
        # 设备选择 :在多 GPU 系统中,你可以使用 torch.cuda.set_device 来选择一个特定的 GPU 来执行你的代码。这可以帮助你管理资源,特别是在需要在不同的 GPU 上运行不同的任务时。
        # 控制 GPU 使用 :通过显式设置设备,你可以确保特定的操作或模型只在你选择的 GPU 上执行,这有助于避免 GPU 资源的冲突。
        # 优化性能 :在某些情况下,选择正确的 GPU 可以优化性能,特别是在 GPU 之间存在性能差异时。
        # 注意事项 :
        # 在调用 torch.cuda.set_device 之后,所有新的 CUDA 张量和模型默认都会在这个设备上创建。
        # 一旦设置了 CUDA 设备,所有的 CUDA 操作都会默认在这个设备上执行,除非你显式地指定另一个设备。
        # 在多线程环境中, torch.cuda.set_device 只影响当前线程。如果你的代码是多线程的,需要在每个线程中设置设备。

        # 设置当前进程的 CUDA 设备为 opt.local_rank 指定的设备。
        torch.cuda.set_device(opt.local_rank)
        # 更新 device 变量,指定使用 CUDA 设备,并且是 opt.local_rank 指定的设备。
        device = torch.device('cuda', opt.local_rank)
        # 初始化进程组,使用 NCCL 作为后端, init_method='env://' 表示使用环境变量来配置进程组。
        dist.init_process_group(backend='nccl', init_method='env://')  # distributed backend
        # 确保 opt.batch_size 可以被 opt.world_size 整除,即每个 GPU 上的批次大小必须是总批次大小的整数分之一。
        assert opt.batch_size % opt.world_size == 0, '--batch-size must be multiple of CUDA device count'    # --batch-size 必须是 CUDA 设备数量的倍数。
        # 更新 opt.batch_size 为每个 GPU 上的批次大小,即总批次大小除以 CUDA 设备的数量。
        opt.batch_size = opt.total_batch_size // opt.world_size
    # 这段代码的目的是确保在分布式训练环境中,每个进程都能够正确地设置其使用的设备,并且批次大小能够均匀分配到每个 GPU 上。通过这种方式,可以确保模型在多个 GPU 上并行训练时的一致性和效率。

    # Hyperparameters
    # 这段代码是用于从 YAML 文件中加载超参数的 Python 代码。
    # 上下文管理器。
    # 这行代码使用 with 语句打开一个文件,这是一种上下文管理器,它确保文件在操作完成后会被正确关闭。
    # opt.hyp 是一个包含超参数文件路径的字符串变量。
    with open(opt.hyp) as f:
        # YAML 加载。
        # yaml.load(f, Loader=yaml.SafeLoader) 是用来从文件中加载 YAML 格式的数据。 f 是前面打开的文件对象。 Loader=yaml.SafeLoader 指定了一个安全的 YAML 加载器,它只加载 YAML 中的基本数据类型,防止执行任意代码,提高安全性。
        # 超参数存储。加载的 YAML 数据被存储在变量 hyp 中,这个变量随后可以在代码中用来访问超参数。
        hyp = yaml.load(f, Loader=yaml.SafeLoader)  # load hyps

    # Train
    # 这段代码是训练流程的一部分,涉及到日志记录、TensorBoard 日志的初始化以及训练函数的调用。
    # 记录配置信息。这行代码使用 logger 对象记录 opt (一个包含训练配置的命名空间对象)的信息。这通常包括训练的参数,如学习率、批次大小、训练周期等。
    logger.info(opt)
    # 检查是否进行超参数演化。如果 opt.evolve 为 False ,则执行以下代码块。 opt.evolve 可能用于控制是否进行超参数演化(一种自动化的超参数优化过程)。
    if not opt.evolve:
        # 初始化 TensorBoard 记录器。初始时,将 tb_writer 设置为 None 。 tb_writer 将用于记录 TensorBoard 日志。
        tb_writer = None  # init loggers
        # 检查全局排名。如果当前进程的全局排名是 -1 或 0 ,则执行以下代码。 -1 通常表示非分布式训练环境,而 0 表示分布式训练中的第一个进程。
        if opt.global_rank in [-1, 0]:
            # 打印 TensorBoard 启动信息。
            # 打印一条信息,告诉用户如何启动 TensorBoard 并查看训练日志。 colorstr 函数可能用于给日志信息添加颜色。
            prefix = colorstr('tensorboard: ')
            logger.info(f"{prefix}Start with 'tensorboard --logdir {opt.project}', view at http://localhost:6006/")
            # 初始化 TensorBoard 写入器。创建一个 SummaryWriter 对象,用于将训练过程中的日志写入到 TensorBoard。 opt.save_dir 是保存日志的目录。
            tb_writer = SummaryWriter(opt.save_dir)  # Tensorboard
        # 调用训练函数。最后,调用 train 函数,传入 超参数 hyp 、 配置 opt 、 设备 device 和 TensorBoard 写入器 tb_writer ,开始训练过程。
        train(hyp, opt, device, tb_writer)

    # Evolve hyperparameters (optional)    进化超参数(可选)。
    # 这段代码处理的是超参数演化(Hyperparameter Evolution)的过程,这是一种自动化的超参数优化技术,用于找到最佳的超参数组合以提高模型性能。
    else:
        # 超参数演化元数据(突变尺度 0-1、lower_limit、upper_limit)。
        # Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit)
        # 初始化超参数演化元数据。 meta 字典包含了每个超参数的演化元数据,包括突变规模、下界和上界。
        # 每个键值对中的键是超参数的名称,值是一个三元组,包含了以下信息 :
        # 变异增益 :第一个元素(1或0)表示变异的相对强度,1 表示该参数可以变异,0 表示该参数不参与变异。
        # 下界 :第二个元素是超参数的最小值,表示在演化过程中该参数的值不能低于此值。
        # 上界 :第三个元素是超参数的最大值,表示在演化过程中该参数的值不能高于此值。
                # lr0  :初始学习率(SGD=1E-2, Adam=1E-3)
        meta = {'lr0': (1, 1e-5, 1e-1),  # initial learning rate (SGD=1E-2, Adam=1E-3)
                # lrf :OneCycleLR 学习率的最终值(lr0 * lrf)。
                'lrf': (1, 0.01, 1.0),  # final OneCycleLR learning rate (lr0 * lrf)
                # momentum :SGD 动量/Adam beta1。
                'momentum': (0.3, 0.6, 0.98),  # SGD momentum/Adam beta1
                # weight_decay :优化器权重衰减。
                'weight_decay': (1, 0.0, 0.001),  # optimizer weight decay
                # warmup_epochs :预热周期(可以接受小数)。
                'warmup_epochs': (1, 0.0, 5.0),  # warmup epochs (fractions ok)
                #  warmup_momentum 。:预热初始动量。
                'warmup_momentum': (1, 0.0, 0.95),  # warmup initial momentum
                # warmup_bias_lr :预热初始偏置学习率。
                'warmup_bias_lr': (1, 0.0, 0.2),  # warmup initial bias lr
                # box :框损失增益。
                'box': (1, 0.02, 0.2),  # box loss gain
                # cls :分类损失增益。
                'cls': (1, 0.2, 4.0),  # cls loss gain
                # cls_pw :分类损失的正样本权重。
                'cls_pw': (1, 0.5, 2.0),  # cls BCELoss positive_weight
                # obj :目标损失增益(与像素数成比例)。
                'obj': (1, 0.2, 4.0),  # obj loss gain (scale with pixels)
                # obj_pw :目标损失的正样本权重。
                'obj_pw': (1, 0.5, 2.0),  # obj BCELoss positive_weight
                # iou_t :IoU 训练阈值。
                'iou_t': (0, 0.1, 0.7),  # IoU training threshold
                # anchor_t :锚点倍数阈值。
                'anchor_t': (1, 2.0, 8.0),  # anchor-multiple threshold
                # anchors :每个输出网格的锚点数(0 表示忽略)。
                'anchors': (2, 2.0, 10.0),  # anchors per output grid (0 to ignore)
                # fl_gamma :焦点损失的 gamma 值(efficientDet 默认 gamma=1.5)。
                'fl_gamma': (0, 0.0, 2.0),  # focal loss gamma (efficientDet default gamma=1.5)
                # hsv_h :图像 HSV-色调增强(分数)。
                'hsv_h': (1, 0.0, 0.1),  # image HSV-Hue augmentation (fraction)
                # hsv_s  :图像 HSV-饱和度增强(分数)。
                'hsv_s': (1, 0.0, 0.9),  # image HSV-Saturation augmentation (fraction)
                # hsv_v :图像 HSV-值增强(分数)。
                'hsv_v': (1, 0.0, 0.9),  # image HSV-Value augmentation (fraction)
                # degrees :图像旋转(+/- 度)。
                'degrees': (1, 0.0, 45.0),  # image rotation (+/- deg)
                # translate :图像平移(+/- 分数)。
                'translate': (1, 0.0, 0.9),  # image translation (+/- fraction)
                # scale :图像缩放(+/- 增益)。
                'scale': (1, 0.0, 0.9),  # image scale (+/- gain)
                # shear :图像剪切(+/- 度)。
                'shear': (1, 0.0, 10.0),  # image shear (+/- deg)
                # perspective :图像透视(+/- 分数),范围 0-0.001。
                'perspective': (0, 0.0, 0.001),  # image perspective (+/- fraction), range 0-0.001
                # flipud :图像上下翻转(概率)。
                'flipud': (1, 0.0, 1.0),  # image flip up-down (probability)
                # fliplr :图像左右翻转(概率)。
                'fliplr': (0, 0.0, 1.0),  # image flip left-right (probability)
                # mosaic :图像混合(概率)。
                'mosaic': (1, 0.0, 1.0),  # image mixup (probability)
                # mixup :图像混合(概率)。
                'mixup': (1, 0.0, 1.0)}  # image mixup (probability)

        # 断言检查。 这行代码使用 assert 语句确保 opt.local_rank 等于 -1 。
        # local_rank 通常用于分布式训练中标识当前进程对应的 GPU。这里 -1 表示没有使用分布式训练(即单 GPU 或非 GPU 环境)。
        # 如果 opt.local_rank 不是 -1 ,则会抛出异常,提示 DDP 模式(分布式数据并行)没有为 --evolve 选项实现。
        assert opt.local_rank == -1, 'DDP mode not implemented for --evolve'    # DDP 模式未实现--evolve。
        # 设置测试和保存选项。
        # 这两行代码将 opt.notest 和 opt.nosave 设置为 True ,意味着在演化过程中不会在每个 epoch 结束时进行测试或保存模型,只在最终 epoch 结束时进行。
        opt.notest, opt.nosave = True, True  # only test/save final epoch
        # ei = [isinstance(x, (int, float)) for x in hyp.values()]  # evolvable indices
        # 定义 YAML 文件路径。 这行代码使用 Path 类(来自 pathlib 模块)构建了一个文件路径,用于保存演化过程中找到的最佳超参数。 opt.save_dir 是保存结果的目录, 'hyp_evolved.yaml' 是文件名。
        yaml_file = Path(opt.save_dir) / 'hyp_evolved.yaml'  # save best result here
        # 下载演化文件。
        if opt.bucket:
            # 如果提供了 opt.bucket (一个 Google Cloud Storage 桶的名称),则使用 os.system 调用 Google Cloud 的 gsutil 命令行工具来下载名为 evolve.txt 的文件。这个文件可能包含了之前演化过程的结果,用于继续或恢复演化过程。
            os.system('gsutil cp gs://%s/evolve.txt .' % opt.bucket)  # download evolve.txt if exists

        # 这段代码是一个超参数演化(Hyperparameter Evolution)的实现,它通过模拟自然选择和遗传算法中的突变过程来优化超参数。
        # 演化代数。这个循环运行300次,每次代表一代演化。
        for _ in range(300):  # generations to evolve
            # 检查 evolve.txt 文件。如果存在 evolve.txt 文件,表示有之前的演化结果,可以在此基础上进行选择和突变。
            if Path('evolve.txt').exists():  # if evolve.txt exists: select best hyps and mutate    如果 evolve.txt 存在:选择最佳 hyps 并进行变异。
                # Select parent(s)    选择父类。
                # 选择方法。确定是使用“单一”选择还是“加权”选择方法。
                parent = 'single'  # parent selection method: 'single' or 'weighted'    父类选择方法:“单一”或“加权”。
                # 加载和排序先前结果。
                # 加载 evolve.txt 文件中的数据,并根据适应度函数 fitness 对结果进行排序,选择前 n 个最好的结果。
                x = np.loadtxt('evolve.txt', ndmin=2)
                n = min(5, len(x))  # number of previous results to consider    要考虑的先前结果的数量。
                # # def fitness(x): -> 用于计算模型的适应度(fitness),作为一个加权组合的度量指标。 -> return (x[:, :4] * w).sum(1)
                x = x[np.argsort(-fitness(x))][:n]  # top n mutations    前 n 个突变。
                # 计算权重。计算每个结果的权重,权重基于结果的适应度。
                w = fitness(x) - fitness(x).min()  # weights    权重。
                # 选择父代。
                # 如果选择“单一”方法,随机选择一个结果
                if parent == 'single' or len(x) == 1:
                    # x = x[random.randint(0, n - 1)]  # random selection
                    x = x[random.choices(range(n), weights=w)[0]]  # weighted selection    加权选择。
                # 如果选择“加权”方法,根据权重计算加权组合。
                elif parent == 'weighted':
                    x = (x * w.reshape(n, 1)).sum(0) / w.sum()  # weighted combination    加权组合。

                # Mutate    合变。
                # 设置变异参数。
                # 设置变异概率 mp 和标准差 s ,初始化随机数生成器。
                mp, s = 0.8, 0.2  # mutation probability, sigma    突变概率,sigma。
                npr = np.random
                npr.seed(int(time.time()))
                # 计算变异因子。
                # 计算变异因子 v ,直到 v 中至少有一个值发生变化,以避免产生重复的超参数组合。
                # 计算变异增益。
                # 这行代码从 meta 字典中提取每个超参数的变异增益(gains),这些增益是变异过程中用于调整变异强度的系数。
                # meta 字典中的每个条目都是一个三元组,其中第一个元素是变异增益,第二个是下界,第三个是上界。
                # g 是一个包含这些增益值的 NumPy 数组。
                g = np.array([x[0] for x in meta.values()])  # gains 0-1
                # 计算超参数数量。ng 是超参数的总数,通过计算 meta 字典的长度得到。
                ng = len(meta)
                # 初始化变异因子数组。 v 是一个长度为 ng 的 NumPy 数组,所有元素初始化为 1。这个数组将存储每个超参数的变异因子。
                v = np.ones(ng)
                # 变异直到发生变化。
                while all(v == 1):  # mutate until a change occurs (prevent duplicates)    不断变异直到发生变化(防止重复)。
                    # 这个 while 循环确保每个超参数至少有一个非1的变异因子,以避免产生与父代完全相同的超参数组合(即防止重复)。
                    # npr.random(ng) < mp 生成一个布尔数组,表示每个超参数是否发生变异(基于变异概率 mp )。
                    # npr.randn(ng) 生成一个正态分布随机数数组,用于模拟变异的随机性。
                    # npr.random() * s 生成一个随机数,用于调整变异的强度(基于标准差 s )。
                    # g * ... + 1 将变异增益 g 应用到上述随机变异上,并加1以确保变异因子始终大于1。
                    # .clip(0.3, 3.0) 确保变异因子 v 在0.3到3.0的范围内,这是一个防止极端变异的措施。
                    v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
                # 遍历超参数。这个循环遍历 hyp 字典中的每个超参数。 i 是索引, k 是超参数的键(名称)。
                for i, k in enumerate(hyp.keys()):  # plt.hist(v.ravel(), 300)
                    # 应用变异。
                    # 对于每个超参数,将其当前值(存储在 x[i + 7] 中)乘以相应的变异因子(存储在 v[i] 中)。
                    # x[i + 7] 是从先前结果中选择的超参数值,偏移7是因为 x 数组的前7列可能包含其他信息(如适应度值)。
                    # v[i] 是为当前超参数生成的变异因子。
                    # 结果被转换为浮点数并赋值回 hyp[k] ,更新超参数的值。
                    hyp[k] = float(x[i + 7] * v[i])  # mutate
            # 这段代码通过模拟自然选择和遗传算法中的选择和变异过程,不断迭代以寻找最优的超参数组合。这种方法可以有效地探索超参数空间,并可能找到比手动调整更优的超参数组合。

            # Constrain to limits    限制。
            # 遍历 meta 字典。这个循环遍历 meta 字典中的每个条目, k 是超参数的名称, v 是一个包含变异增益、下界和上界的元组。
            for k, v in meta.items():
                # 应用下界和上界。
                # 对于每个超参数,确保其值不会低于下界 v[1] ,也不会高于上界 v[2] 。这防止了超参数超出预定义的范围。
                hyp[k] = max(hyp[k], v[1])  # lower limit
                hyp[k] = min(hyp[k], v[2])  # upper limit

                # round(number, ndigits=None)
                # Python 中的 round 函数用于将一个浮点数四舍五入到指定的小数位数。如果省略小数位数参数,则默认四舍五入到最近的整数。
                # 参数 :
                # number :要四舍五入的数字。
                # ndigits :可选参数,指定要四舍五入到的小数位数。如果设置为 None ,则 number 将被四舍五入到最近的整数。
                # 返回值 :
                # 返回四舍五入后的数字。

                # 保留有效数字。将超参数的值四舍五入到5位有效数字。这有助于保持数值的稳定性和可比性。
                hyp[k] = round(hyp[k], 5)  # significant digits    有效数字。

            # Train mutation    训练突变。
            # 训练突变后的模型。
            # 使用突变后的超参数 hyp.copy() (创建 hyp 的副本以避免修改原始超参数)进行模型训练。 opt 包含训练的配置选项, device 指定了训练的设备(如 GPU)。
            # train 函数执行训练过程,并返回训练结果。
            results = train(hyp.copy(), opt, device)

            # Write mutation results    写入突变结果。
            # 记录突变结果。
            # print_mutation 函数 用于记录当前突变的超参数和训练结果。
            # def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''): -> 用于处理和记录超参数进化(hyperparameter evolution)的结果。
            print_mutation(hyp.copy(), results, yaml_file, opt.bucket)

        # Plot results
        # 用于可视化演化结果并提供如何使用最佳超参数重新训练模型的指令。
        # 绘制演化结果。
        # 这行代码调用了一个名为 plot_evolution 的函数,该函数读取 yaml_file 文件中记录的最佳超参数和相应的性能结果,然后使用这些数据生成图表。这个图表展示了随着代数增加,性能指标(如准确率、损失等)的变化趋势,以及最终选择的最佳超参数组合。
        # def plot_evolution(yaml_file='data/hyp.finetune.yaml'): -> 用于绘制超参数演化结果,这些结果通常存储在 evolve.txt 文件中。这个函数可以帮助用户可视化不同超参数组合的性能变化。
        plot_evolution(yaml_file)
        # 打印完成信息和训练命令。
        print(f'Hyperparameter evolution complete. Best results saved as: {yaml_file}\n'    # 超参数演化完成。最佳结果保存为:{yaml_file}。
              f'Command to train a new model with these hyperparameters: $ python train.py --hyp {yaml_file}')    # 使用这些超参数训练新模型的命令:$ python train.py --hyp {yaml_file}。
    # 这段代码实现了一个完整的超参数演化流程,通过自动化的方式寻找最佳的超参数组合,以提高模型的性能。这个过程涉及到多次训练和评估,以及对超参数的调整和记录。


http://www.kler.cn/a/398276.html

相关文章:

  • 机器学习-36-对ML的思考之机器学习研究的初衷及科学研究的期望
  • 【CANOE】【学习】【DecodeString】字节转为中文字符输出
  • 微服务链路追踪skywalking安装
  • 网络基础(4)传输层
  • MySQL Online DDL
  • 手机ip地址异常怎么解决
  • SQLite 安装指南
  • MAC上的Office三件套报53错误解决方案(随笔记)
  • 【MogDB】MogDB5.2.0重磅发布第八篇-支持PLSQL编译全局缓存
  • 如何在 Ubuntu 上安装 Mattermost 团队协作工具
  • 【ArcGIS微课1000例】0127:计算城市之间的距离
  • 9.2 使用haarcascade_frontalface_default.xml分类器检测视频中的人脸,并框出人脸位置。
  • 企业项目级IDEA设置类注释、方法注释模板(仅增加@author和@date)
  • 你的服务器缓存中毒过么?
  • Essential Cell Biology--Fifth Edition--Chapter one (8)
  • ssm126基于HTML5的出租车管理系统+jsp(论文+源码)_kaic
  • 牛客周赛第一题2024/11/17日
  • 深入理解Flutter生命周期函数之StatefulWidget(一)
  • 【Qt聊天室】客户端实现总结
  • 华为欧拉系统使用U盘制作引导安装华为欧拉操作系统
  • Kubernetes 10 问,测测你对 k8s 的理解程度
  • 【设计模式】入门 23 种设计模式(代码讲解)
  • 在linux里如何利用vim对比两个文档不同的行数
  • 小智的疑惑——决赛4 #传智
  • PHP框架 单一入口和多入口以及优缺点
  • WPF Gif图谱 如果隐藏的话会存在BUG