YOLOv5源码解读1.3-训练train.py

 往期回顾:YOLOv5源码解读1.0-目录_汉卿HanQ的博客-CSDN博客


目录

1.导入Python库

2.获取文件路径

3.自定义模块

4.分布式训练初始化(多GPU)

5.train

5.1创建训练权重路径 设置模型 txt等保存路径

5.2读取超参数配置文件

5.3设置参数保存路径

5.4加载日志信息

5.5加载其他参数

5.6加载网络训练模型

5.7冻结层

5.8设置训练和测试图片尺寸

5.9设置一次训练所选取的样本数

5.10设置优化器

5.11设置学习率衰减方式

5.12训练前最后准备

5.13创建数据集

5.14 计算anchor

5.15初始化训练需要的模型参数

5.16 训练前热身

5.17开始训练

5.17.1通过model.train进入训练阶段

5.17.2释放开始训练命令和更新权重

5.17.3分布式训练设置以及控制台显示

5.17.4分批加载数据和热身训练

5.17.5正向传播 反向传播 更新参数

5.17.6打印相关信息 做权重衰减 进入下一轮训练

5.17.7训练完成保存模型

5.17.8打印信息并释放显存

6.设置OPT

7.run函数

8.main函数

8.1检查分布式训练环境

8.2判断是否断点续训练

8.3判断是否分布式训练

8.4判断超参数进化

9.整体代码


1.导入Python库

import argparse  # 解析命令行参数模块
import math  # 数学公式模块
import os  # 与操作系统进行交互的模块 包含文件路径操作和解析
import random  # 生成随机数模块
import sys  # sys系统模块 包含了与Python解释器和它的环境有关的函数
import time  # 时间模块 更底层
from copy import deepcopy  # 深度拷贝模块
from datetime import datetime  # datetime模块能以更方便的格式显示日期或对日期进行运算。
from pathlib import Path  # Path将str转换为Path对象 使字符串路径易于操作的模块

import numpy as np  # numpy数组操作模块
import torch  # 引入torch
import torch.distributed as dist  # 分布式训练模块
import torch.nn as nn  # 对torch.nn.functional的类的封装 有很多和torch.nn.functional相同的函数
import yaml  # yaml是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互。一般用于存储配置文件。
from torch.cuda import amp  # PyTorch amp自动混合精度训练模块
from torch.nn.parallel import DistributedDataParallel as DDP  # 多卡训练模块
from torch.optim import SGD, Adam, lr_scheduler  # tensorboard模块
from tqdm import tqdm  # 进度条模块


2.获取文件路径

# ----------------------------------2.获取文件路径----------------------------------
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0]  # YOLOv5 root directory
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))  # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative

3.自定义模块

# ----------------------------------3.自定义模块----------------------------------
import val  # 测试集
from models.experimental import attempt_load # 实验性质代码 包括MixConv2d 跨层权重Sum
from models.yolo import Model # yolo特点模块 包括BaseModel DetectionModel ClassificationModel parse_model
from utils.autoanchor import check_anchors # 定义自动生成anchor方法
from utils.autobatch import check_train_batch_size #自动生成批量大小的方法
from utils.callbacks import Callbacks # 回调函数 主要为logger服务
from utils.datasets import create_dataloader # dataset和dataloader代码
from utils.downloads import attempt_download # google云盘内容下载
from utils.general import (LOGGER, NCOLS, check_dataset, check_file, check_git_status, check_img_size,
                           check_requirements, check_suffix, check_yaml, colorstr, get_latest_run, increment_path,
                           init_seeds, intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods,
                           one_cycle, print_args, print_mutation, strip_optimizer) #常用工具函数
from utils.loggers import Loggers # 打印日志
from utils.loggers.wandb.wandb_utils import check_wandb_resume
from utils.loss import ComputeLoss # 存放损失函数
from utils.metrics import fitness # 模型验证指标 ap混淆矩阵等
from utils.plots import plot_evolve, plot_labels # Annotator类 可以在图像上绘制矩形框和标注西悉尼
from utils.torch_utils import EarlyStopping, ModelEMA, de_parallel, select_device, torch_distributed_zero_first # 定义了与Pytorch有关工具函数 选择设备 同步时间等


4.分布式训练初始化(多GPU)

# ----------------------------------4.分布式训练初始化(多GPU)----------------------------------
"""
    查考这两个环境变量,rank和local_rank区别在于前者用于进程间通讯, 后者用于本地设备分配
"""
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1))  #本地序列号,这个worker是这台机器上第几个worker
RANK = int(os.getenv('RANK', -1)) # 进程序号,这个Worker是全局第几个worker
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) # 总共有几个worker

5.train

# ----------------------------------5.train----------------------------------
def train(hyp,  # path/to/hyp.yaml or hyp dictionary
          opt,
          device,
          callbacks
          ):
    save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze, = \
        Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \
        opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze
    # ----------------------------------5.1 创建训练权重路径 设置模型 txt等保存路径----------------------------------
    # Directories
    w = save_dir / 'weights'  # 权重保存路径
    (w.parent if evolve else w).mkdir(parents=True, exist_ok=True)  # 新建文件夹
    last, best = w / 'last.pt', w / 'best.pt' # 保存训练结果 last.pt和best.pt
5.1创建训练权重路径 设置模型 txt等保存路径
# ----------------------------------5.1 创建训练权重路径 设置模型 txt等保存路径----------------------------------
    # Directories
    w = save_dir / 'weights'  # 权重保存路径
    (w.parent if evolve else w).mkdir(parents=True, exist_ok=True)  # 新建文件夹
    last, best = w / 'last.pt', w / 'best.pt' # 保存训练结果 last.pt和best.pt
5.2读取超参数配置文件
# ----------------------------------5.2 读取超参数配置文件----------------------------------
    # Hyperparameters
    if isinstance(hyp, str): # 判断hyp是字典还是字符串
        with open(hyp, errors='ignore') as f: # 若字符串(路径),则加载超参数为字典
            hyp = yaml.safe_load(f)  # load hyps dict 加载yaml超参数文件
    LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) # 打印
5.3设置参数保存路径
# ----------------------------------5.3 设置参数保存路径----------------------------------
    # Save run settings
    with open(save_dir / 'hyp.yaml', 'w') as f: # 保存超参数为yaml配置文件
        yaml.safe_dump(hyp, f, sort_keys=False) # sefe_dump将yaml文件序列化
    with open(save_dir / 'opt.yaml', 'w') as f: # 保存命令行参数为yaml配置文件
        yaml.safe_dump(vars(opt), f, sort_keys=False) # vars是将namespace数据转换为字典
    data_dict = None # 定义数据字典
5.4加载日志信息
# ----------------------------------5.4 加载日志信息----------------------------------
    # Loggers
    """
        设置wandb和tb两种可视化工具
    """
    if RANK in [-1, 0]: # 如果编号 -1/0
        loggers = Loggers(save_dir, weights, opt, hyp, LOGGER)  # 初始化日志记录器
        if loggers.wandb:
            data_dict = loggers.wandb.data_dict
            if resume: # 如果训练终端,再读取一次参数
                weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp

        # Register actions
        for k in methods(loggers):
            callbacks.register_action(k, callback=getattr(loggers, k)) # 将日志记录器中的方法与字符串绑定
5.5加载其他参数
# ----------------------------------5.5 加载其他参数----------------------------------
    # Config
    """
        画图开关,cuda,随机种子,读取数据集相关yaml文件
    """
    plots = not evolve  # s师傅绘制训练 测试图片
    cuda = device.type != 'cpu'
    init_seeds(1 + RANK) # 设置随机种子
    # 加载数据配置信息
    with torch_distributed_zero_first(LOCAL_RANK): # 同步所有进程
        data_dict = data_dict or check_dataset(data)  # 检查数据集
    train_path, val_path = data_dict['train'], data_dict['val'] # 训练数据集,测试集图片路径
    nc = 1 if single_cls else int(data_dict['nc'])  # 数据集种类数量
    names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names']  #  类别所有名字
    assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}'  # 判断类别长度和文件是否对应
    is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt')  # 当前数据集是否是coco数据集
5.6加载网络训练模型
# ----------------------------------5.6 加载网络训练模型----------------------------------
    # Model
    check_suffix(weights, '.pt')  # 判断文件后缀是否是.pt
    pretrained = weights.endswith('.pt') # 加载预训练权重
    if pretrained:
        # 使用预训练 torch_distributed_zero_first是用于同步不同进程对数据读取的上下文管理器
        with torch_distributed_zero_first(LOCAL_RANK):
            # 如果本地不存在就从google云盘下载
            weights = attempt_download(weights)  # download if not found locally
        ckpt = torch.load(weights, map_location=device)  # 加载模型和参数
        model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # 加载模型
        exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else []  # 获取anchor 若cfg噶一gto不为空且不终端训练,设置exclude=anchor
        csd = ckpt['model'].float().state_dict()  # 将预训练模型中的所有参数保存下来给csd
        csd = intersect_dicts(csd, model.state_dict(), exclude=exclude)  # 判断参数多少相同,筛选字典中键值对,把exclude删除
        model.load_state_dict(csd, strict=False)  # 模型创建
        LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}')  # 显示加载与训练权重的键值对和创建模型的键值对 如果pre=true则减少两个键值对
    else:
        model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # 直接加载模型 ch为输入图片通道
5.7冻结层
# ----------------------------------5.7 冻结层---------------------------------
    """
        其作用是冰冻一些层,使得这些层再反向传播时不再更新权重 需要冻结的层可以卸载freeze列表中
    """
    # Freeze
    freeze = [f'model.{x}.' for x in range(freeze)]  # layers to freeze
    # 遍历所有层
    for k, v in model.named_parameters():
        v.requires_grad = True  # 把所有层参数设置梯度
        if any(x in k for x in freeze): # 判断是否冻结
            LOGGER.info(f'freezing {k}')
            v.requires_grad = False # 冻结训练的层梯度不进行更新
5.8设置训练和测试图片尺寸
 # ----------------------------------5.8 设置训练和测试图片尺寸---------------------------------
    # Image size
    gs = max(int(model.stride.max()), 32)  # 获取模型总步长和图片分辨率
    imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2)  # 检查图片是否能被32整除
5.9设置一次训练所选取的样本数
# ----------------------------------5.9 设置一次训练所选取的样本数--------------------------------
    # Batch size
    if RANK == -1 and batch_size == -1:  # single-GPU only, estimate best batch size
        batch_size = check_train_batch_size(model, imgsz) #确保batch size满足要求
5.10设置优化器
# ----------------------------------5.10 设置优化器---------------------------------
    # Optimizer
    nbs = 64  # nbs是nominal batch size ,名义上的batch_size 命令行bs是16
    accumulate = max(round(nbs / batch_size), 1)  # 累计次数 计算opt中bs多少批才能达到nbs水平
    hyp['weight_decay'] *= batch_size * accumulate / nbs  # 根据accumulate设置权重衰减参数 防止过拟合
    LOGGER.info(f"Scaled weight_decay = {hyp['weight_decay']}") # 打印缩放后的权重衰减超参数

    """
        因为bs发生变化,所偶权重参数也要进行相应缩放
    """
    g0, g1, g2 = [], [], []  # 将模型分为三组 BN的weight 卷积层的weight biases 进行分组优化
    """
        g0表示归一化层中所有权重参数
        g1表示卷积层中所有权重参数
        g2表示所有的偏置参数
    """
    for v in model.modules(): # 遍历网络中的所有层,每遍历一层向更深的层遍历
        if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):  # 指定对象中是否有给定的属性
            g2.append(v.bias) # 将层的bias添加至g2
        if isinstance(v, nn.BatchNorm2d):  # weight (no decay)
            g0.append(v.weight) # 将BN层的权重添加至g0 未经过权重衰减
        elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):  # weight (with decay)
            g1.append(v.weight) # 将层的weight添加至g1 经过了权重衰减 这里是指卷积层

    # 选用优化器,并设置g0(bn)组的优化方法
    """
        首先判断是否使用adam优化器,初始参数为批归一化层中的参数,如果不适用adam优化器,则直接使用SGD随机梯度下降
        然后将g1 g2添加至优化器
    """
    if opt.adam:
        optimizer = Adam(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))  # adjust beta1 to momentum
    else:
        optimizer = SGD(g0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)

    # 将卷积层的参数添加至优化器,并做权重衰减
    optimizer.add_param_group({'params': g1, 'weight_decay': hyp['weight_decay']})  # add_param_group添加一个参数组,同一个优化器可以更新很多个参数组,不用的参数组可以设置不同的超参数
    optimizer.add_param_group({'params': g2})  # add g2 (biases)# 将所有的bias添加至优化器
    LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__} with parameter groups "
                f"{len(g0)} weight, {len(g1)} weight (no decay), {len(g2)} bias") # 打印优化信息
    del g0, g1, g2 # 在内存中删除g0 g1 g2释放空间
5.11设置学习率衰减方式
# ----------------------------------5.11 设置学习率衰减方式---------------------------------
    # Scheduler
    """
        yolov5设置两种学习率策略 线性学习率和余弦退火率
    """
    if opt.linear_lr: # 线性学习率
        lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp['lrf']) + hyp['lrf']  # linear
    else: # 余弦退火率
        lf = one_cycle(1, hyp['lrf'], epochs)  # cosine 1->hyp['lrf']
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)  # 可视化 scheduler
5.12训练前最后准备
 # ----------------------------------5.12 训练前最后准备---------------------------------
    """
        EMA 断点续训 迭代次数的架子啊 DP SyncBatchNorm
    """
    # EMA
    """
        EMA为指数加权平均或滑动平均。其将前面模型训练权重,偏差进行保存,在本次训练过程中,假设为第n次
        将第一次到第n-1次以指数权重进行加和,再加上本次的结果,且越远离第n次,指数系数越大,其所占的比重越小。
    """
    ema = ModelEMA(model) if RANK in [-1, 0] else None # 指数平均加权或滑动平均,考虑历史值对参数的影响,目的使收敛的曲线更加平滑

    # Resume
    """
        断点续训。可以理解为把上次中断结束时的模型,作为新的预训练模型,然后从中获取上次训练时的参数
        并恢复训练状态。·epoch迭代次数。1个epoch等于使用训练集中的全部样本训练一次,epoch的大小
        跟迭代次数有着密切的关系,通常在迭代次数处于2000-3000之间损失已经处于平稳。
    """
    start_epoch, best_fitness = 0, 0.0
    if pretrained: # 如果有预训练
        # Optimizer 加载优化器与best_fitness
        if ckpt['optimizer'] is not None:
            optimizer.load_state_dict(ckpt['optimizer']) # 将预训练模型中的参数加载到优化器
            best_fitness = ckpt['best_fitness'] # 预训练模型中最佳fitnees 保存为best.pt bf=0.0*p+0.0*r+0.1*mAP+0.9*mAP@0.5:0.95

        # EMA
        # 加载ema模型和updates参数 保持ema的平滑性
        if ema and ckpt.get('ema'):
            ema.ema.load_state_dict(ckpt['ema'].float().state_dict())
            ema.updates = ckpt['updates']

        # Epochs 加载训练的迭代次数
        start_epoch = ckpt['epoch'] + 1 # 从上次的epoch接着训练
        if resume:
            assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.'
        if epochs < start_epoch:# 如果新epoch小于加载的epoch 则新epoch为需要再训练的轮次数而不是总的论次数
            # 如果训练次数小于开始论次数 则打印日志恢复训练
            LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.")
            epochs += ckpt['epoch']  # 计算新的轮次

        del ckpt, csd # 释放内存

    # DP mode
    """
        DP mode。DataParallel单机多卡模式自动将数据切分 load 到相应 GPU,将模型复制到相应 GPU,进行正向传播计算梯度并汇总。
        值得注意的是,模型和数据都需要先导入进GPU中,DataParallel 的module才能对其进行处理,否则会报错
        s使用单机多卡训练一般不使用
        rank为进程编号 如果rank=-1&&count?1 则使用DataParallel单机多卡模式 效果并不好
        rank=-1&&count=1 不会进行分布式
    """
    if cuda and RANK == -1 and torch.cuda.device_count() > 1:
        LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n'
                       'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
        model = torch.nn.DataParallel(model)

    # SyncBatchNorm 多卡归一化
    """
        SyncBatchNorm。SyncBatchNorm主要用于解决多卡归一化同步问题,每张卡单独计算均值
        然后同步,得到全局均值。用全局均值计算每张卡的方差,然后同步即可得到全局方差,但两次会消耗时间挺长。
    """
    if opt.sync_bn and cuda and RANK != -1: # 多卡训练,把不同卡的数据做个同步
        model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
        LOGGER.info('Using SyncBatchNorm()')
5.13创建数据集
# ----------------------------------5.13 创建数据集---------------------------------
    # Trainloader
    # 返回一个训练数据加载器,一个数据集对象。训练数据加载器是一个可迭代的对象 可以通过for循环加载1个batch_size的数据
    # 数据集对象包括数据集的一些参数,包括标签值 所有训练数据路径 每张图片的尺寸等
    # create_dataloader得到两个对象train_loader训练数据加载器 dataset数据集对象
    train_loader, dataset = create_dataloader(train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls,
                                              hyp=hyp, augment=True, cache=opt.cache, rect=opt.rect, rank=LOCAL_RANK,
                                              workers=workers, image_weights=opt.image_weights, quad=opt.quad,
                                              prefix=colorstr('train: '), shuffle=True)

    mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max())  # 标签编号最大值
    nb = len(train_loader)  # 类别总数
    # 如果小于类别则表示有问题
    assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'

    # Process 0 验证数据集加载
    if RANK in [-1, 0]:
        val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls,
                                       hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1,
                                       workers=workers, pad=0.5,
                                       prefix=colorstr('val: '))[0]
        # 加载验证集数据加载器
        if not resume: # 如果没有使用 resume
            labels = np.concatenate(dataset.labels, 0) # 统计dataset的label信息
            # c = torch.tensor(labels[:, 0])  # classes
            # cf = torch.bincount(c.long(), minlength=nc) + 1.  # frequency
            # model._initialize_biases(cf.to(device))
            if plots: # 画出标签信息
                plot_labels(labels, names, save_dir)
5.14 计算anchor
# ----------------------------------5.14 计算anchor---------------------------------
            # Anchors
            """
                计算anchor与数据集标签的长宽比
            """
            if not opt.noautoanchor:
                check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz)
                """
                    check_anchors 计算默认锚点anchor与数据集标签的长宽比值,标签的长h宽w与anchor的h_a w_a的壁纸,如果标签框满足条件总数的99%,则根据k-mean算法聚类新的锚点
                    dataset是训练集
                    thr=hyp['anchor_t'] 是从配置文件hpy.scratch.ua,;第七的超参数 其中anchor_t当配置文件中的anchor计算bpr(best possible recall)大于0.98才计算新anchor
                    小于0.98 程序会根据数据集的label自动学习anchor的尺寸
                """
            model.half().float()  # 半进度

        callbacks.run('on_pretrain_routine_end') # 每个训练前例行程序结束时出发所有已注册的回调

    # DDP mode
    if cuda and RANK != -1:
        model = DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
5.15初始化训练需要的模型参数
# ----------------------------------5.15 初始化训练需要的模型参数---------------------------------
    # Model attributes
    nl = de_parallel(model).model[-1].nl  # 根据自己数据集的类别和网络FPN层设置各个损失稀疏
    hyp['box'] *= 3 / nl  # box为预测框损失
    hyp['cls'] *= nc / 80 * 3 / nl  # cls为分类损失
    hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl  # obj为置信度损失
    hyp['label_smoothing'] = opt.label_smoothing # 标签平滑
    model.nc = nc  # 设置模型的类别,然后将检测的类别个数保存到模型
    model.hyp = hyp  # 设置模型超参数,然后保存到模型
    model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc  # 从训练样本标签得到类别权重,然后将类别权重保存至模型
    model.names = names # 获取类别的名字,然后将分类标签保存到模型
5.16 训练前热身
# ----------------------------------5.16 训练前热身---------------------------------
    # Start training
    t0 = time.time() #获取当前时间
    nw = max(round(hyp['warmup_epochs'] * nb), 1000)  # 获取热身训练的迭代次数
    # nw = min(nw, (epochs - start_epoch) / 2 * nb)  # limit warmup to < 1/2 of training
    last_opt_step = -1
    # 初始化mAP和results
    maps = np.zeros(nc)  # mAP per class
    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  # 设置学习率衰减所进行到的伦茨,即使打断训练,使用resume接着训练也能正常衔接之前的训练进行学习率衰减
    scaler = amp.GradScaler(enabled=cuda) # 设置amp混合精度训练
    stopper = EarlyStopping(patience=opt.patience) # 早停止 不更新结束训练
    compute_loss = ComputeLoss(model)  # init loss class # 初始化损失函数
    LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n' # 打印训练和测试输入图片分辨率
                f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' # 加载图片时调用cu进程数
                f"Logging results to {colorstr('bold', save_dir)}\n" # 日志目录 
                f'Starting training for {epochs} epochs...') # 从哪个eppch开始训练
5.17开始训练
# ----------------------------------5.17 开始训练---------------------------------
    """
        1.通过model.train进入训练阶段
        2.释放开始训练命令和更新权重
        3.分布式训练设置以及控制台显示
        4.分批加载数据和热身训练
        5.正向传播 反向传播 更新参数
        6.打印相关信息 做权重衰减 进入下一轮训练
        7.训练完成保存模型
        8.打印信息并释放显存
    """
5.17.1通过model.train进入训练阶段
# *****************************1.通过model.train进入训练阶段***********************************
        model.train()
5.17.2释放开始训练命令和更新权重
# *****************************2.释放开始训练命令和更新权重***********************************
        # Update image weights (optional, single-GPU only)
        if opt.image_weights: # 获取图片的权重
            # 经过一轮训练 若哪一类的精度第那么这个类就会肥胖一个较高的权重,来增加它被采样的概率
            cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc  # class weights
            # 将计算出的权重换算到图片的维度,将类别的权重换算为图片的权重
            iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw)  # image weights
            # 通过 random.choices生成图片索引indices从而进行采样,这时图像会包含一些难识别的样本
            dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)  # rand weighted idx
5.17.3分布式训练设置以及控制台显示
# *****************************3.分布式训练设置以及控制台显示***********************************
        mloss = torch.zeros(3, device=device)  # mean losses
        # DDP模式打乱数据,并且dpp.sampler的随机采样是基于epoch+seed作为随机种子,每次都不同
        if RANK != -1:
            train_loader.sampler.set_epoch(epoch)
        pbar = enumerate(train_loader) # 将训练数据迭代器做枚举,可以遍历出索引值
        LOGGER.info(('\n' + '%10s' * 7) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'labels', 'img_size')) #训练参数表头
        if RANK in [-1, 0]:
            pbar = tqdm(pbar, total=nb, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # 通过tqdm创建进度条,方便训练信息展示
        optimizer.zero_grad() # 将优化器中所有参数梯度设为0
5.17.4分批加载数据和热身训练
# *****************************4.分批加载数据和热身训练***********************************
        for i, (imgs, targets, paths, _) in pbar:  # batch -------------------------------------------------------------
            ni = i + nb * epoch  # 计算当前迭代次数
            imgs = imgs.to(device, non_blocking=True).float() / 255  # 将图片加载至设备并做归一化

            # Warmup 热身训练
            if ni <= nw:
                xi = [0, nw]  # x interp
                # compute_loss.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 / batch_size]).round())
                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
                    x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
                    """
                        bias的学习率从0.1下降到lr*lf(epoch)
                        其他参数学习率从0增加到lr*lf(epoch)
                        lf为上面设置的余弦退火衰减函数
                    """
                    if 'momentum' in x:
                        x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])

            # Multi-scale 多尺度训练 从imagsz(默认尺寸)*0.5  imgsz*1.5+gs(模型最大stride=32)随机选取尺寸
            if opt.multi_scale:
                sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs  # 随机改变图片尺寸
                sf = sz / max(imgs.shape[2:])  # scale factor
                if sf != 1:
                    ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]]  # new shape (stretched to gs-multiple)
                    imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) # 下采样
5.17.5正向传播 反向传播 更新参数
 # *****************************5.前向传播 反向传播 更新参数***********************************
            # Forward 前向传播
            with amp.autocast(enabled=cuda):
                pred = model(imgs)  # 将图片送入网络得到一个预测结果
                # loss为总损失 loss_items是一个损失元组
                loss, loss_items = compute_loss(pred, targets.to(device))  # 计算损失 包括分类,objectness,框的损失
                if RANK != -1:
                    loss *= WORLD_SIZE  # DDP训练,平均不同gpu之间的梯度
                if opt.quad:
                    loss *= 4. # 如果采用collate-Fn4取出的mosaic4数据loss也要翻倍*4

            # Backward 反向传播
            scaler.scale(loss).backward() # scale为使用自动混合京都运算

            # Optimize 模型会对多批数据进行累计,累积到一定次数更新参数,
            if ni - last_opt_step >= accumulate:
                scaler.step(optimizer)  # 参数更新 首先把梯度值unscale返回 如果梯度不是infs/NaNs调用optimizer.step更新权重 否则不更新权重
                scaler.update() # 更新参数
                optimizer.zero_grad() # 完成一次累计后梯度清零
                if ema:
                    ema.update(model)
                last_opt_step = ni # 计数
5.17.6打印相关信息 做权重衰减 进入下一轮训练
 *****************************6.打印相关信息,做权重衰减***********************************
            # Log 打印相关信息
            if RANK in [-1, 0]:
                # 打印显存 伦茨 损失 target数量 图片尺寸等
                mloss = (mloss * i + loss_items) / (i + 1)  # update mean losses
                mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G'  # 计算显存
                pbar.set_description(('%10s' * 2 + '%10.4g' * 5) % (
                    f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1])) # 进度条显示 #将日志记录并生成一些记录图片
                callbacks.run('on_train_batch_end', ni, model, imgs, targets, paths, plots, opt.sync_bn)
            # end batch ------------------------------------------------------------------------------------------------

        # Scheduler 学习率衰减
        lr = [x['lr'] for x in optimizer.param_groups]  # for loggers
        scheduler.step() # 根据前面设置的学习率更新策略更新学习率


        if RANK in [-1, 0]:
            # mAP
            callbacks.run('on_train_epoch_end', epoch=epoch)
            ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) # model属性给ema
            final_epoch = (epoch + 1 == epochs) or stopper.possible_stop# 判断当前epoch是否最后一轮
            if not noval or final_epoch:  #
                """
                   测试使用的是ema(指数移动平均 对模型的参数做平均)的模型
                      results: [1] Precision 所有类别的平均precision(最大f1时)
                               [1] Recall 所有类别的平均recall
                               [1] map@0.5 所有类别的平均mAP@0.5
                               [1] map@0.5:0.95 所有类别的平均mAP@0.5:0.95
                               [1] box_loss 验证集回归损失, obj_loss 验证集置信度损失, cls_loss 验证集分类损失
                      maps: [80] 所有类别的mAP@0.5:0.95
                """
                results, maps, _ = val.run(data_dict,
                                           batch_size=batch_size // WORLD_SIZE * 2,# 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
                                           imgsz=imgsz, # 要保证batch_size能整除卡数
                                           model=ema.ema,
                                           single_cls=single_cls,# 是否是单类数据集
                                           dataloader=val_loader,
                                           save_dir=save_dir,# 保存地址 runs/train/expn
                                           plots=False,# 是否可视化
                                           callbacks=callbacks,
                                           compute_loss=compute_loss)# 损失函数(train)

            # Update best mAP
            fi = fitness(np.array(results).reshape(1, -1))  # fi: [P, R, mAP@.5, mAP@.5-.95]的一个加权值 = 0.1*mAP@.5 + 0.9*mAP@.5-.95
            if fi > best_fitness: # 当前fi大于最佳的
                best_fitness = fi # 更新
            log_vals = list(mloss) + list(results) + lr #保存验证结果
            callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) # 保存验证数据
5.17.7训练完成保存模型
# *****************************7.保存模型***********************************
            # Save model
            """
               保存带checkpoint的模型用于inference或resuming training
               保存模型, 还保存了epoch, results, optimizer等信息
               optimizer将不会在最后一轮完成后保存
               model保存的是EMA的模型
            """
            if (not nosave) or (final_epoch and not evolve):  # if save
                # 将打钱训练过程中的所有参数赋值给ckpt
                ckpt = {'epoch': epoch,
                        'best_fitness': best_fitness,
                        'model': deepcopy(de_parallel(model)).half(),
                        'ema': deepcopy(ema.ema).half(),
                        'updates': ema.updates,
                        'optimizer': optimizer.state_dict(),
                        'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None,
                        'date': datetime.now().isoformat()}

                # Save last, best and delete
                torch.save(ckpt, last) # 保存每轮的模型
                if best_fitness == fi: # 如果是最佳的模型
                    torch.save(ckpt, best)
                if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0):
                    torch.save(ckpt, w / f'epoch{epoch}.pt') # 保存模型
                del ckpt # 释放内存
                callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) # 保存到日志

            # Stop Single-GPU 停止单卡训练
            if RANK == -1 and stopper(epoch=epoch, fitness=fi):
                break

            # Stop DDP TODO: known issues shttps://github.com/ultralytics/yolov5/pull/4576
            # stop = stopper(epoch=epoch, fitness=fi)
            # if RANK == 0:
            #    dist.broadcast_object_list([stop], 0)  # broadcast 'stop' to all ranks

        # Stop DPP
        # with torch_distributed_zero_first(RANK):
        # if stop:
        #    break  # must break all DDP ranks

        # end epoch ----------------------------------------------------------------------------------------------------
    # end training -----------------------------------------------------------------------------------------------------
5.17.8打印信息并释放显存
# *****************************8.打印信息并释放显存***********************************
    if RANK in [-1, 0]:
        LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.')# 停止训练,向控制台输出信息
        for f in last, best:
            if f.exists():
                strip_optimizer(f)  # 训练完成后 讲过optimizer从ckpt中删除
                if f is best:
                    LOGGER.info(f'\nValidating {f}...') # 把最好的模型在验证集跑一边 并绘图
                    results, _, _ = val.run(data_dict,
                                            batch_size=batch_size // WORLD_SIZE * 2,
                                            imgsz=imgsz,
                                            model=attempt_load(f, device).half(),
                                            iou_thres=0.65 if is_coco else 0.60,  # best pycocotools results at 0.65
                                            single_cls=single_cls,
                                            dataloader=val_loader,
                                            save_dir=save_dir,
                                            save_json=is_coco,
                                            verbose=True,
                                            plots=True,
                                            callbacks=callbacks,
                                            compute_loss=compute_loss)  # val best model with plots
                    if is_coco: # 如果hi是coco数据集
                        callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi)
        # 记录训练终止时日志
        callbacks.run('on_train_end', last, best, plots, epoch, results)
        LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}")

    torch.cuda.empty_cache() # 释放显存
    return results

6.设置OPT

# ----------------------------------6 opt---------------------------------
def parse_opt(known=False):
    parser = argparse.ArgumentParser()
    # weights: 预训练权重文件
    parser.add_argument('--weights', type=str, default=ROOT / 'pretrained/yolov5s.pt', help='initial weights path')
    # cfg: 训练模型 模型配置文件 网络结构
    parser.add_argument('--cfg', type=str, default=ROOT / 'models/yolov5s.yaml', help='model.yaml path')
    # data: 训练路径,包括训练集,验证集,测试集的路径,类别总数等
    parser.add_argument('--data', type=str, default=ROOT / 'data/fire_data.yaml', help='dataset.yaml path')
    # hpy: 超参数设置文件(lr/sgd/mixup)./data/hyps/下面有5个超参数设置文件,每个文件的超参数初始值有细微区别,用户可以根据自己的需求选择其中一个
    parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch.yaml', help='hyperparameters path')
    # epochs: 训练轮次, 默认轮次为300次
    parser.add_argument('--epochs', type=int, default=300)
    # batchsize: 训练批次, 默认bs=16
    parser.add_argument('--batch-size', type=int, default=4, help='total batch size for all GPUs, -1 for autobatch')
    # imagesize: 设置图片大小, 默认640*640
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
    # rect: 是否采用矩形训练,默认为False
    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: 不保存模型  默认False(保存)  在./runs/exp*/train/weights/保存两个模型 一个是最后一次的模型 一个是最好的模型
    parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
    # noval: 最后进行测试, 设置了之后就是训练结束都测试一下, 不设置每轮都计算mAP, 建议不设置
    parser.add_argument('--noval', action='store_true', help='only validate final epoch')
    # noautoanchor: 不自动调整anchor, 默认False, 自动调整anchor
    parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
    # evolve: 参数进化, 遗传算法调参
    parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
    # bucket: 谷歌优盘 / 一般用不到
    parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
    # cache: 是否提前缓存图片到内存,以加快训练速度,默认False
    parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"')
    # mage-weights: 使用图片采样策略,默认不使用
    parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
    # device: 设备选择
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    # parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
    # multi-scale 是否进行多尺度训练
    parser.add_argument('--multi-scale', default=True, help='vary img-size +/- 50%%')
    # single-cls: 数据集是否多类/默认True
    parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
    # optimizer: 优化器选择 / 提供了三种优化器
    parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
    # sync-bn: 是否使用跨卡同步BN,在DDP模式使用
    parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
    # dataloader的最大worker数量 (使用多线程加载图片)
    parser.add_argument('--workers', type=int, default=0, help='max dataloader workers (per RANK in DDP mode)')
    # 训练结果的保存路径
    parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
    # 训练结果的文件名称
    parser.add_argument('--name', default='exp', help='save to project/name')
    # 项目位置是否存在 / 默认是都不存在
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    # 四元数据加载器: 允许在较低 --img 尺寸下进行更高 --img 尺寸训练的一些好处。
    parser.add_argument('--quad', action='store_true', help='quad dataloader')
    # cos-lr: 余弦学习率
    parser.add_argument('--linear-lr', action='store_true', help='linear LR')
    # 标签平滑 / 默认不增强, 用户可以根据自己标签的实际情况设置这个参数,建议设置小一点 0.1 / 0.05
    parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
    # 早停止耐心次数 / 100次不更新就停止训练
    parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
    # --freeze冻结训练 可以设置 default = [0] 数据量大的情况下,建议不设置这个参数
    parser.add_argument('--freeze', type=int, default=0, help='Number of layers to freeze. backbone=10, all=24')
    # --save-period 多少个epoch保存一下checkpoint
    parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
    # --local_rank 进程编号 / 多卡使用
    parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
    # Weights & Biases arguments
    # 在线可视化工具,类似于tensorboard工具
    parser.add_argument('--entity', default=None, help='W&B: Entity')
    # upload_dataset: 是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集) 默认False
    parser.add_argument('--upload_dataset', action='store_true', help='W&B: Upload dataset as artifact table')
    # bbox_interval: 设置界框图像记录间隔 Set bounding-box image logging interval for W&B 默认-1   opt.epochs // 10
    parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval')
    # 使用数据的版本
    parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')
    # 作用就是当仅获取到基本设置时,如果运行命令中传入了之后才会获取到的其他配置,不会报错;而是将多出来的部分保存起来,留到后面使用
    opt = parser.parse_known_args()[0] if known else parser.parse_args()
    return opt

7.run函数

# ----------------------------------7. run---------------------------------
def run(**kwargs):
    # Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolov5m.pt')
    # 执行这个脚本/调用train函数/开启训练
    """
        使得支持指令执行脚本 与主函数差不多 为导入时提供
    """
    opt = parse_opt(True)
    for k, v in kwargs.items():
        setattr(opt, k, v) # 属性赋值,属性不存在则创建一个赋值
    main(opt)

8.main函数

# ----------------------------------8. main---------------------------------
def main(opt, callbacks=Callbacks()):
8.1检查分布式训练环境
    # ----------------------------------8.1 检查分布式训练环境---------------------------------
    # Checks
    if RANK in [-1, 0]:
        print_args(FILE.stem, opt) # 以彩色方式输出所有训练参数
        check_git_status() # 检查github库是否更新
8.2判断是否断点续训练
# ----------------------------------8.2 判断是否断点续训练---------------------------------
    # Resume
    if opt.resume and not check_wandb_resume(opt) and not opt.evolve:  # resume an interrupted run
        ckpt = opt.resume if isinstance(opt.resume, str) else get_latest_run()  # 找到最近的权重文件last.pt
        assert os.path.isfile(ckpt), 'ERROR: --resume checkpoint does not exist' # 判断是否是文件,若不是则抛出文件异常
        with open(Path(ckpt).parent.parent / 'opt.yaml', errors='ignore') as f: # opt.yaml是训练时的命令行参数问及那
            opt = argparse.Namespace(**yaml.safe_load(f))  # 超参数替换,将训练时的命令行参数加载到opt参数对象中
        opt.cfg, opt.weights, opt.resume = '', ckpt, True  # 设置为‘’ 对应train函数里面的操作 即加载权重时是否加载anchor
        LOGGER.info(f'Resuming training from {ckpt}') # 打印从ckpt恢复断点训练信息
    else:
        # 不适用断点续训,从文件中读取相关参数
        opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \
            check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project)  # checks
        assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' #如果模型文件和权重文件为空 弹出警告
        if opt.evolve:
            opt.project = str(ROOT / 'runs/evolve') # 设置心得项目输出目录
            opt.exist_ok, opt.resume = opt.resume, False  # 将resume传递给exiist_ok
        opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) #根据opt.project生成目睹并给opt.save_dir
8.3判断是否分布式训练
# ----------------------------------8.3 判断是否分布式训练---------------------------------
    """
        DDP用于单机或多级的多GPU分布式训练,但目前DDP只能在linux使用,
    """
    # DDP mode
    device = select_device(opt.device, batch_size=opt.batch_size) # 选择程序装载的位置
    if LOCAL_RANK != -1: # 当进程内的GPU!=-1 进度DDP
        assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' # 用于DDP训练GPU数量不足
        assert opt.batch_size % WORLD_SIZE == 0, '--batch-size must be multiple of CUDA device count' # world_size表示全局进程数
        assert not opt.image_weights, '--image-weights argument is not compatible with DDP training' # 不能使用图片采样策略
        assert not opt.evolve, '--evolve argument is not compatible with DDP training' # 不能使用超参数进化
        torch.cuda.set_device(LOCAL_RANK) # 设置装载程序设备
        device = torch.device('cuda', LOCAL_RANK) # 保存装载程序谁不
        dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo") # 用于多GPU训练模块
8.4判断超参数进化
# ----------------------------------8.4 判断是否超参数进化训练---------------------------------
    """
        遗传算法,每次保存最优结果,但至少需要300次
            1.若存在evolve.csv 读取文件中训练数据,选择超参进化方式,结果最优的训练数据突变超参数
            2.限制超参进化参数hyp在规定范围内
            3.使用突变后的超参数进行训练,测试其结果
            4.训练结束后,将训练可视化并保存在evolution.csv
        遗传算法4个参数:群体大学 进化次数 交叉概率 变异概率
    """
    # Train
    if not opt.evolve:  #判断是否进行进化
        train(opt.hyp, opt, device, callbacks) # 直接从train开始
        if WORLD_SIZE > 1 and RANK == 0: # 如果全局进程大于1并且rank=0
            LOGGER.info('Destroying process group... ') # 日志输出,摧毁进程组
            dist.destroy_process_group() # 训练完毕,销毁所有进程

    # Evolve hyperparameters (optional)
    else:
        # 超参数列表范围
        meta = {'lr0': (1, 1e-5, 1e-1),  # initial learning rate (SGD=1E-2, Adam=1E-3)
                'lrf': (1, 0.01, 1.0),  # final OneCycleLR learning rate (lr0 * lrf)
                'momentum': (0.3, 0.6, 0.98),  # SGD momentum/Adam beta1
                'weight_decay': (1, 0.0, 0.001),  # optimizer weight decay
                'warmup_epochs': (1, 0.0, 5.0),  # warmup epochs (fractions ok)
                'warmup_momentum': (1, 0.0, 0.95),  # warmup initial momentum
                'warmup_bias_lr': (1, 0.0, 0.2),  # warmup initial bias lr
                'box': (1, 0.02, 0.2),  # box loss gain
                'cls': (1, 0.2, 4.0),  # cls loss gain
                'cls_pw': (1, 0.5, 2.0),  # cls BCELoss positive_weight
                'obj': (1, 0.2, 4.0),  # obj loss gain (scale with pixels)
                'obj_pw': (1, 0.5, 2.0),  # obj BCELoss positive_weight
                'iou_t': (0, 0.1, 0.7),  # IoU training threshold
                'anchor_t': (1, 2.0, 8.0),  # anchor-multiple threshold
                'anchors': (2, 2.0, 10.0),  # anchors per output grid (0 to ignore)
                'fl_gamma': (0, 0.0, 2.0),  # focal loss gamma (efficientDet default gamma=1.5)
                'hsv_h': (1, 0.0, 0.1),  # image HSV-Hue augmentation (fraction)
                'hsv_s': (1, 0.0, 0.9),  # image HSV-Saturation augmentation (fraction)
                'hsv_v': (1, 0.0, 0.9),  # image HSV-Value augmentation (fraction)
                'degrees': (1, 0.0, 45.0),  # image rotation (+/- deg)
                'translate': (1, 0.0, 0.9),  # image translation (+/- fraction)
                'scale': (1, 0.0, 0.9),  # image scale (+/- gain)
                'shear': (1, 0.0, 10.0),  # image shear (+/- deg)
                'perspective': (0, 0.0, 0.001),  # image perspective (+/- fraction), range 0-0.001
                'flipud': (1, 0.0, 1.0),  # image flip up-down (probability)
                'fliplr': (0, 0.0, 1.0),  # image flip left-right (probability)
                'mosaic': (1, 0.0, 1.0),  # image mixup (probability)
                'mixup': (1, 0.0, 1.0),  # image mixup (probability)
                'copy_paste': (1, 0.0, 1.0)}  # segment copy-paste (probability)

        with open(opt.hyp, errors='ignore') as f:
            hyp = yaml.safe_load(f)  # load hyps dict
            if 'anchors' not in hyp:  # 如果超参数中没有anchor 则设置为3
                hyp['anchors'] = 3
        opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir)  # 使用遗传算法时,仅在最后的epoch测试和保存
        # ei = [isinstance(x, (int, float)) for x in hyp.values()]  # evolvable indices
        evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv'
        if opt.bucket:
            os.system(f'gsutil cp gs://{opt.bucket}/evolve.csv {save_dir}')  # download evolve.csv if exists
        # 迭代次数 默认300
        for _ in range(opt.evolve):  # generations to evolve
            if evolve_csv.exists():  # if evolve.csv 存在
                # Select parent(s)
                parent = 'single'  # 选择进化方式 有: 'single' or 'weighted'
                x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1) # j加载 evolve.txt
                n = min(5, len(x))  # 选取至多前五次进化结果
                x = x[np.argsort(-fitness(x))][:n]  # argsort从小到大排序 fitness为x前4项(p r mAP mAP:0.95)加权
                w = fitness(x) - fitness(x).min() + 1E-6  # 根据mp mr mAP50 mAP的加权和作为权重计算hyp权重
                if parent == 'single' or len(x) == 1: # 根据不同进化方式获得base hyp
                    # x = x[random.randint(0, n - 1)]  # random selection
                    x = x[random.choices(range(n), weights=w)[0]]  # 根据权重的几率随机挑选适应历史前5的其中一个
                elif parent == 'weighted':
                    x = (x * w.reshape(n, 1)).sum(0) / w.sum()  # 对hyp乘上对应的权重融合曾一个hpy 再取平均 /权重和

                # Mutate 突变 超参数进化
                mp, s = 0.8, 0.2  # 设置图片概率
                npr = np.random
                npr.seed(int(time.time())) # 根据时间设置随机数种子 伪随机
                g = np.array([meta[k][0] for k in hyp.keys()])  # 获取突变初始值(meta三个值中的第一个) meta=[变异初始概率,最低限制,最大限制]
                ng = len(meta)
                v = np.ones(ng) # 确保其中至少有一个参数超变异了
                while all(v == 1):  # 设置突变
                    v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
                for i, k in enumerate(hyp.keys()):  # 将突变添加到base hyp上
                    hyp[k] = float(x[i + 7] * v[i])  # mutate

            # Constrain to limits 限制hpy在规定范围内
            for k, v in meta.items():
                # hyp是超参数配置文件对象 kv是在元超参数中遍历出来的(是元组),hyp的v是一个数
                hyp[k] = max(hyp[k], v[1])  # 限定最小值 选择二者之间大的,防止hyp值过小
                hyp[k] = min(hyp[k], v[2])  # 限定最大值 选二者之间最小的
                hyp[k] = round(hyp[k], 5)  # 四舍五入到小数点后五位
                # 最后得到的值是hyo中的值与meta的最大值之间小的

            # Train mutation 使用突变后的超参数 测试结果
            results = train(hyp.copy(), opt, device, callbacks)

            # Write mutation results
            # 将结果写入到results中,并将byp写入到evolve.txt(每一行为一次进化结果)
            # 其中evolve.txt每一行为(p,r,mAP.F1,test_losses(GIOU,obj,cls))之后为hyp 最后将hyp保存到yaml文件中
            print_mutation(results, hyp.copy(), save_dir, opt.bucket)

        # Plot results 结果可视化
        plot_evolve(evolve_csv)
        LOGGER.info(f'Hyperparameter evolution finished\n'
                    f"Results saved to {colorstr('bold', save_dir)}\n"
                    f'Use best hyperparameters example: $ python train.py --hyp {evolve_yaml}')

9.整体代码

# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Train a YOLOv5 model on a custom dataset

Usage:
    $ python path/to/train.py --data coco128.yaml --weights yolov5s.pt --img 640
"""
# ----------------------------------1.导入Python库----------------------------------
import argparse  # 解析命令行参数模块
import math  # 数学公式模块
import os  # 与操作系统进行交互的模块 包含文件路径操作和解析
import random  # 生成随机数模块
import sys  # sys系统模块 包含了与Python解释器和它的环境有关的函数
import time  # 时间模块 更底层
from copy import deepcopy  # 深度拷贝模块
from datetime import datetime  # datetime模块能以更方便的格式显示日期或对日期进行运算。
from pathlib import Path  # Path将str转换为Path对象 使字符串路径易于操作的模块

import numpy as np  # numpy数组操作模块
import torch  # 引入torch
import torch.distributed as dist  # 分布式训练模块
import torch.nn as nn  # 对torch.nn.functional的类的封装 有很多和torch.nn.functional相同的函数
import yaml  # yaml是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互。一般用于存储配置文件。
from torch.cuda import amp  # PyTorch amp自动混合精度训练模块
from torch.nn.parallel import DistributedDataParallel as DDP  # 多卡训练模块
from torch.optim import SGD, Adam, lr_scheduler  # tensorboard模块
from tqdm import tqdm  # 进度条模块

# ----------------------------------2.获取文件路径----------------------------------
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0]  # YOLOv5 root directory
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))  # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative


# ----------------------------------3.自定义模块----------------------------------
import val  # 测试集
from models.experimental import attempt_load # 实验性质代码 包括MixConv2d 跨层权重Sum
from models.yolo import Model # yolo特点模块 包括BaseModel DetectionModel ClassificationModel parse_model
from utils.autoanchor import check_anchors # 定义自动生成anchor方法
from utils.autobatch import check_train_batch_size #自动生成批量大小的方法
from utils.callbacks import Callbacks # 回调函数 主要为logger服务
from utils.datasets import create_dataloader # dataset和dataloader代码
from utils.downloads import attempt_download # google云盘内容下载
from utils.general import (LOGGER, NCOLS, check_dataset, check_file, check_git_status, check_img_size,
                           check_requirements, check_suffix, check_yaml, colorstr, get_latest_run, increment_path,
                           init_seeds, intersect_dicts, labels_to_class_weights, labels_to_image_weights, methods,
                           one_cycle, print_args, print_mutation, strip_optimizer) #常用工具函数
from utils.loggers import Loggers # 打印日志
from utils.loggers.wandb.wandb_utils import check_wandb_resume
from utils.loss import ComputeLoss # 存放损失函数
from utils.metrics import fitness # 模型验证指标 ap混淆矩阵等
from utils.plots import plot_evolve, plot_labels # Annotator类 可以在图像上绘制矩形框和标注西悉尼
from utils.torch_utils import EarlyStopping, ModelEMA, de_parallel, select_device, torch_distributed_zero_first # 定义了与Pytorch有关工具函数 选择设备 同步时间等

# ----------------------------------4.分布式训练初始化(多GPU)----------------------------------
"""
    查考这两个环境变量,rank和local_rank区别在于前者用于进程间通讯, 后者用于本地设备分配
"""
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1))  #本地序列号,这个worker是这台机器上第几个worker
RANK = int(os.getenv('RANK', -1)) # 进程序号,这个Worker是全局第几个worker
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) # 总共有几个worker

# ----------------------------------5.train----------------------------------
def train(hyp,  # path/to/hyp.yaml or hyp dictionary
          opt,
          device,
          callbacks
          ):
    save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze, = \
        Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \
        opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze
    # ----------------------------------5.1 创建训练权重路径 设置模型 txt等保存路径----------------------------------
    # Directories
    w = save_dir / 'weights'  # 权重保存路径
    (w.parent if evolve else w).mkdir(parents=True, exist_ok=True)  # 新建文件夹
    last, best = w / 'last.pt', w / 'best.pt' # 保存训练结果 last.pt和best.pt

    # ----------------------------------5.2 读取超参数配置文件----------------------------------
    # Hyperparameters
    if isinstance(hyp, str): # 判断hyp是字典还是字符串
        with open(hyp, errors='ignore') as f: # 若字符串(路径),则加载超参数为字典
            hyp = yaml.safe_load(f)  # load hyps dict 加载yaml超参数文件
    LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) # 打印

    # ----------------------------------5.3 设置参数保存路径----------------------------------
    # Save run settings
    with open(save_dir / 'hyp.yaml', 'w') as f: # 保存超参数为yaml配置文件
        yaml.safe_dump(hyp, f, sort_keys=False) # sefe_dump将yaml文件序列化
    with open(save_dir / 'opt.yaml', 'w') as f: # 保存命令行参数为yaml配置文件
        yaml.safe_dump(vars(opt), f, sort_keys=False) # vars是将namespace数据转换为字典
    data_dict = None # 定义数据字典
    # ----------------------------------5.4 加载日志信息----------------------------------
    # Loggers
    """
        设置wandb和tb两种可视化工具
    """
    if RANK in [-1, 0]: # 如果编号 -1/0
        loggers = Loggers(save_dir, weights, opt, hyp, LOGGER)  # 初始化日志记录器
        if loggers.wandb:
            data_dict = loggers.wandb.data_dict
            if resume: # 如果训练终端,再读取一次参数
                weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp

        # Register actions
        for k in methods(loggers):
            callbacks.register_action(k, callback=getattr(loggers, k)) # 将日志记录器中的方法与字符串绑定

    # ----------------------------------5.5 加载其他参数----------------------------------
    # Config
    """
        画图开关,cuda,随机种子,读取数据集相关yaml文件
    """
    plots = not evolve  # s师傅绘制训练 测试图片
    cuda = device.type != 'cpu'
    init_seeds(1 + RANK) # 设置随机种子
    # 加载数据配置信息
    with torch_distributed_zero_first(LOCAL_RANK): # 同步所有进程
        data_dict = data_dict or check_dataset(data)  # 检查数据集
    train_path, val_path = data_dict['train'], data_dict['val'] # 训练数据集,测试集图片路径
    nc = 1 if single_cls else int(data_dict['nc'])  # 数据集种类数量
    names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names']  #  类别所有名字
    assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}'  # 判断类别长度和文件是否对应
    is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt')  # 当前数据集是否是coco数据集

    # ----------------------------------5.6 加载网络训练模型----------------------------------
    # Model
    check_suffix(weights, '.pt')  # 判断文件后缀是否是.pt
    pretrained = weights.endswith('.pt') # 加载预训练权重
    if pretrained:
        # 使用预训练 torch_distributed_zero_first是用于同步不同进程对数据读取的上下文管理器
        with torch_distributed_zero_first(LOCAL_RANK):
            # 如果本地不存在就从google云盘下载
            weights = attempt_download(weights)  # download if not found locally
        ckpt = torch.load(weights, map_location=device)  # 加载模型和参数
        model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # 加载模型
        exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else []  # 获取anchor 若cfg噶一gto不为空且不终端训练,设置exclude=anchor
        csd = ckpt['model'].float().state_dict()  # 将预训练模型中的所有参数保存下来给csd
        csd = intersect_dicts(csd, model.state_dict(), exclude=exclude)  # 判断参数多少相同,筛选字典中键值对,把exclude删除
        model.load_state_dict(csd, strict=False)  # 模型创建
        LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}')  # 显示加载与训练权重的键值对和创建模型的键值对 如果pre=true则减少两个键值对
    else:
        model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # 直接加载模型 ch为输入图片通道

    # ----------------------------------5.7 冻结层---------------------------------
    """
        其作用是冰冻一些层,使得这些层再反向传播时不再更新权重 需要冻结的层可以卸载freeze列表中
    """
    # Freeze
    freeze = [f'model.{x}.' for x in range(freeze)]  # layers to freeze
    # 遍历所有层
    for k, v in model.named_parameters():
        v.requires_grad = True  # 把所有层参数设置梯度
        if any(x in k for x in freeze): # 判断是否冻结
            LOGGER.info(f'freezing {k}')
            v.requires_grad = False # 冻结训练的层梯度不进行更新
    # ----------------------------------5.8 设置训练和测试图片尺寸---------------------------------
    # Image size
    gs = max(int(model.stride.max()), 32)  # 获取模型总步长和图片分辨率
    imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2)  # 检查图片是否能被32整除

    # ----------------------------------5.9 设置一次训练所选取的样本数--------------------------------
    # Batch size
    if RANK == -1 and batch_size == -1:  # single-GPU only, estimate best batch size
        batch_size = check_train_batch_size(model, imgsz) #确保batch size满足要求

    # ----------------------------------5.10 设置优化器---------------------------------
    # Optimizer
    nbs = 64  # nbs是nominal batch size ,名义上的batch_size 命令行bs是16
    accumulate = max(round(nbs / batch_size), 1)  # 累计次数 计算opt中bs多少批才能达到nbs水平
    hyp['weight_decay'] *= batch_size * accumulate / nbs  # 根据accumulate设置权重衰减参数 防止过拟合
    LOGGER.info(f"Scaled weight_decay = {hyp['weight_decay']}") # 打印缩放后的权重衰减超参数

    """
        因为bs发生变化,所偶权重参数也要进行相应缩放
    """
    g0, g1, g2 = [], [], []  # 将模型分为三组 BN的weight 卷积层的weight biases 进行分组优化
    """
        g0表示归一化层中所有权重参数
        g1表示卷积层中所有权重参数
        g2表示所有的偏置参数
    """
    for v in model.modules(): # 遍历网络中的所有层,每遍历一层向更深的层遍历
        if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):  # 指定对象中是否有给定的属性
            g2.append(v.bias) # 将层的bias添加至g2
        if isinstance(v, nn.BatchNorm2d):  # weight (no decay)
            g0.append(v.weight) # 将BN层的权重添加至g0 未经过权重衰减
        elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):  # weight (with decay)
            g1.append(v.weight) # 将层的weight添加至g1 经过了权重衰减 这里是指卷积层

    # 选用优化器,并设置g0(bn)组的优化方法
    """
        首先判断是否使用adam优化器,初始参数为批归一化层中的参数,如果不适用adam优化器,则直接使用SGD随机梯度下降
        然后将g1 g2添加至优化器
    """
    if opt.adam:
        optimizer = Adam(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))  # adjust beta1 to momentum
    else:
        optimizer = SGD(g0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)

    # 将卷积层的参数添加至优化器,并做权重衰减
    optimizer.add_param_group({'params': g1, 'weight_decay': hyp['weight_decay']})  # add_param_group添加一个参数组,同一个优化器可以更新很多个参数组,不用的参数组可以设置不同的超参数
    optimizer.add_param_group({'params': g2})  # add g2 (biases)# 将所有的bias添加至优化器
    LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__} with parameter groups "
                f"{len(g0)} weight, {len(g1)} weight (no decay), {len(g2)} bias") # 打印优化信息
    del g0, g1, g2 # 在内存中删除g0 g1 g2释放空间

    # ----------------------------------5.11 设置学习率衰减方式---------------------------------
    # Scheduler
    """
        yolov5设置两种学习率策略 线性学习率和余弦退火率
    """
    if opt.linear_lr: # 线性学习率
        lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp['lrf']) + hyp['lrf']  # linear
    else: # 余弦退火率
        lf = one_cycle(1, hyp['lrf'], epochs)  # cosine 1->hyp['lrf']
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)  # 可视化 scheduler

    # ----------------------------------5.12 训练前最后准备---------------------------------
    """
        EMA 断点续训 迭代次数的架子啊 DP SyncBatchNorm
    """
    # EMA
    """
        EMA为指数加权平均或滑动平均。其将前面模型训练权重,偏差进行保存,在本次训练过程中,假设为第n次
        将第一次到第n-1次以指数权重进行加和,再加上本次的结果,且越远离第n次,指数系数越大,其所占的比重越小。
    """
    ema = ModelEMA(model) if RANK in [-1, 0] else None # 指数平均加权或滑动平均,考虑历史值对参数的影响,目的使收敛的曲线更加平滑

    # Resume
    """
        断点续训。可以理解为把上次中断结束时的模型,作为新的预训练模型,然后从中获取上次训练时的参数
        并恢复训练状态。·epoch迭代次数。1个epoch等于使用训练集中的全部样本训练一次,epoch的大小
        跟迭代次数有着密切的关系,通常在迭代次数处于2000-3000之间损失已经处于平稳。
    """
    start_epoch, best_fitness = 0, 0.0
    if pretrained: # 如果有预训练
        # Optimizer 加载优化器与best_fitness
        if ckpt['optimizer'] is not None:
            optimizer.load_state_dict(ckpt['optimizer']) # 将预训练模型中的参数加载到优化器
            best_fitness = ckpt['best_fitness'] # 预训练模型中最佳fitnees 保存为best.pt bf=0.0*p+0.0*r+0.1*mAP+0.9*mAP@0.5:0.95

        # EMA
        # 加载ema模型和updates参数 保持ema的平滑性
        if ema and ckpt.get('ema'):
            ema.ema.load_state_dict(ckpt['ema'].float().state_dict())
            ema.updates = ckpt['updates']

        # Epochs 加载训练的迭代次数
        start_epoch = ckpt['epoch'] + 1 # 从上次的epoch接着训练
        if resume:
            assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.'
        if epochs < start_epoch:# 如果新epoch小于加载的epoch 则新epoch为需要再训练的轮次数而不是总的论次数
            # 如果训练次数小于开始论次数 则打印日志恢复训练
            LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.")
            epochs += ckpt['epoch']  # 计算新的轮次

        del ckpt, csd # 释放内存

    # DP mode
    """
        DP mode。DataParallel单机多卡模式自动将数据切分 load 到相应 GPU,将模型复制到相应 GPU,进行正向传播计算梯度并汇总。
        值得注意的是,模型和数据都需要先导入进GPU中,DataParallel 的module才能对其进行处理,否则会报错
        s使用单机多卡训练一般不使用
        rank为进程编号 如果rank=-1&&count?1 则使用DataParallel单机多卡模式 效果并不好
        rank=-1&&count=1 不会进行分布式
    """
    if cuda and RANK == -1 and torch.cuda.device_count() > 1:
        LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n'
                       'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
        model = torch.nn.DataParallel(model)

    # SyncBatchNorm 多卡归一化
    """
        SyncBatchNorm。SyncBatchNorm主要用于解决多卡归一化同步问题,每张卡单独计算均值
        然后同步,得到全局均值。用全局均值计算每张卡的方差,然后同步即可得到全局方差,但两次会消耗时间挺长。
    """
    if opt.sync_bn and cuda and RANK != -1: # 多卡训练,把不同卡的数据做个同步
        model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
        LOGGER.info('Using SyncBatchNorm()')

    # ----------------------------------5.13 创建数据集---------------------------------
    # Trainloader
    # 返回一个训练数据加载器,一个数据集对象。训练数据加载器是一个可迭代的对象 可以通过for循环加载1个batch_size的数据
    # 数据集对象包括数据集的一些参数,包括标签值 所有训练数据路径 每张图片的尺寸等
    # create_dataloader得到两个对象train_loader训练数据加载器 dataset数据集对象
    train_loader, dataset = create_dataloader(train_path, imgsz, batch_size // WORLD_SIZE, gs, single_cls,
                                              hyp=hyp, augment=True, cache=opt.cache, rect=opt.rect, rank=LOCAL_RANK,
                                              workers=workers, image_weights=opt.image_weights, quad=opt.quad,
                                              prefix=colorstr('train: '), shuffle=True)

    mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max())  # 标签编号最大值
    nb = len(train_loader)  # 类别总数
    # 如果小于类别则表示有问题
    assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'

    # Process 0 验证数据集加载
    if RANK in [-1, 0]:
        val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls,
                                       hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1,
                                       workers=workers, pad=0.5,
                                       prefix=colorstr('val: '))[0]
        # 加载验证集数据加载器
        if not resume: # 如果没有使用 resume
            labels = np.concatenate(dataset.labels, 0) # 统计dataset的label信息
            # c = torch.tensor(labels[:, 0])  # classes
            # cf = torch.bincount(c.long(), minlength=nc) + 1.  # frequency
            # model._initialize_biases(cf.to(device))
            if plots: # 画出标签信息
                plot_labels(labels, names, save_dir)

            # ----------------------------------5.14 计算anchor---------------------------------
            # Anchors
            """
                计算anchor与数据集标签的长宽比
            """
            if not opt.noautoanchor:
                check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz)
                """
                    check_anchors 计算默认锚点anchor与数据集标签的长宽比值,标签的长h宽w与anchor的h_a w_a的壁纸,如果标签框满足条件总数的99%,则根据k-mean算法聚类新的锚点
                    dataset是训练集
                    thr=hyp['anchor_t'] 是从配置文件hpy.scratch.ua,;第七的超参数 其中anchor_t当配置文件中的anchor计算bpr(best possible recall)大于0.98才计算新anchor
                    小于0.98 程序会根据数据集的label自动学习anchor的尺寸
                """
            model.half().float()  # 半进度

        callbacks.run('on_pretrain_routine_end') # 每个训练前例行程序结束时出发所有已注册的回调

    # DDP mode
    if cuda and RANK != -1:
        model = DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
    # ----------------------------------5.15 初始化训练需要的模型参数---------------------------------
    # Model attributes
    nl = de_parallel(model).model[-1].nl  # 根据自己数据集的类别和网络FPN层设置各个损失稀疏
    hyp['box'] *= 3 / nl  # box为预测框损失
    hyp['cls'] *= nc / 80 * 3 / nl  # cls为分类损失
    hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl  # obj为置信度损失
    hyp['label_smoothing'] = opt.label_smoothing # 标签平滑
    model.nc = nc  # 设置模型的类别,然后将检测的类别个数保存到模型
    model.hyp = hyp  # 设置模型超参数,然后保存到模型
    model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc  # 从训练样本标签得到类别权重,然后将类别权重保存至模型
    model.names = names # 获取类别的名字,然后将分类标签保存到模型

    # ----------------------------------5.16 训练前热身---------------------------------
    # Start training
    t0 = time.time() #获取当前时间
    nw = max(round(hyp['warmup_epochs'] * nb), 1000)  # 获取热身训练的迭代次数
    # nw = min(nw, (epochs - start_epoch) / 2 * nb)  # limit warmup to < 1/2 of training
    last_opt_step = -1
    # 初始化mAP和results
    maps = np.zeros(nc)  # mAP per class
    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  # 设置学习率衰减所进行到的伦茨,即使打断训练,使用resume接着训练也能正常衔接之前的训练进行学习率衰减
    scaler = amp.GradScaler(enabled=cuda) # 设置amp混合精度训练
    stopper = EarlyStopping(patience=opt.patience) # 早停止 不更新结束训练
    compute_loss = ComputeLoss(model)  # init loss class # 初始化损失函数
    LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n' # 打印训练和测试输入图片分辨率
                f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' # 加载图片时调用cu进程数
                f"Logging results to {colorstr('bold', save_dir)}\n" # 日志目录 
                f'Starting training for {epochs} epochs...') # 从哪个eppch开始训练
    # ----------------------------------5.17 开始训练---------------------------------
    """
        1.通过model.train进入训练阶段
        2.释放开始训练命令和更新权重
        3.分布式训练设置以及控制台显示
        4.分批加载数据和热身训练
        5.正向传播 反向传播 更新参数
        6.打印相关信息 做权重衰减 进入下一轮训练
        7.训练完成保存模型
        8.打印信息并释放显存
    """

    for epoch in range(start_epoch, epochs):  # epoch ------------------------------------------------------------------
        # *****************************1.通过model.train进入训练阶段***********************************
        model.train()
        # *****************************2.释放开始训练命令和更新权重***********************************
        # Update image weights (optional, single-GPU only)
        if opt.image_weights: # 获取图片的权重
            # 经过一轮训练 若哪一类的精度第那么这个类就会肥胖一个较高的权重,来增加它被采样的概率
            cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc  # class weights
            # 将计算出的权重换算到图片的维度,将类别的权重换算为图片的权重
            iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw)  # image weights
            # 通过 random.choices生成图片索引indices从而进行采样,这时图像会包含一些难识别的样本
            dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)  # rand weighted idx

        # Update mosaic border (optional)
        # b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs)
        # dataset.mosaic_border = [b - imgsz, -b]  # height, width borders
        # *****************************3.分布式训练设置以及控制台显示***********************************
        mloss = torch.zeros(3, device=device)  # mean losses
        # DDP模式打乱数据,并且dpp.sampler的随机采样是基于epoch+seed作为随机种子,每次都不同
        if RANK != -1:
            train_loader.sampler.set_epoch(epoch)
        pbar = enumerate(train_loader) # 将训练数据迭代器做枚举,可以遍历出索引值
        LOGGER.info(('\n' + '%10s' * 7) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'labels', 'img_size')) #训练参数表头
        if RANK in [-1, 0]:
            pbar = tqdm(pbar, total=nb, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # 通过tqdm创建进度条,方便训练信息展示
        optimizer.zero_grad() # 将优化器中所有参数梯度设为0

        # *****************************4.分批加载数据和热身训练***********************************
        for i, (imgs, targets, paths, _) in pbar:  # batch -------------------------------------------------------------
            ni = i + nb * epoch  # 计算当前迭代次数
            imgs = imgs.to(device, non_blocking=True).float() / 255  # 将图片加载至设备并做归一化

            # Warmup 热身训练
            if ni <= nw:
                xi = [0, nw]  # x interp
                # compute_loss.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 / batch_size]).round())
                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
                    x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
                    """
                        bias的学习率从0.1下降到lr*lf(epoch)
                        其他参数学习率从0增加到lr*lf(epoch)
                        lf为上面设置的余弦退火衰减函数
                    """
                    if 'momentum' in x:
                        x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])

            # Multi-scale 多尺度训练 从imagsz(默认尺寸)*0.5  imgsz*1.5+gs(模型最大stride=32)随机选取尺寸
            if opt.multi_scale:
                sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs  # 随机改变图片尺寸
                sf = sz / max(imgs.shape[2:])  # scale factor
                if sf != 1:
                    ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]]  # new shape (stretched to gs-multiple)
                    imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) # 下采样
            # *****************************5.前向传播 反向传播 更新参数***********************************
            # Forward 前向传播
            with amp.autocast(enabled=cuda):
                pred = model(imgs)  # 将图片送入网络得到一个预测结果
                # loss为总损失 loss_items是一个损失元组
                loss, loss_items = compute_loss(pred, targets.to(device))  # 计算损失 包括分类,objectness,框的损失
                if RANK != -1:
                    loss *= WORLD_SIZE  # DDP训练,平均不同gpu之间的梯度
                if opt.quad:
                    loss *= 4. # 如果采用collate-Fn4取出的mosaic4数据loss也要翻倍*4

            # Backward 反向传播
            scaler.scale(loss).backward() # scale为使用自动混合京都运算

            # Optimize 模型会对多批数据进行累计,累积到一定次数更新参数,
            if ni - last_opt_step >= accumulate:
                scaler.step(optimizer)  # 参数更新 首先把梯度值unscale返回 如果梯度不是infs/NaNs调用optimizer.step更新权重 否则不更新权重
                scaler.update() # 更新参数
                optimizer.zero_grad() # 完成一次累计后梯度清零
                if ema:
                    ema.update(model)
                last_opt_step = ni # 计数
            # *****************************6.打印相关信息,做权重衰减***********************************
            # Log 打印相关信息
            if RANK in [-1, 0]:
                # 打印显存 伦茨 损失 target数量 图片尺寸等
                mloss = (mloss * i + loss_items) / (i + 1)  # update mean losses
                mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G'  # 计算显存
                pbar.set_description(('%10s' * 2 + '%10.4g' * 5) % (
                    f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1])) # 进度条显示 #将日志记录并生成一些记录图片
                callbacks.run('on_train_batch_end', ni, model, imgs, targets, paths, plots, opt.sync_bn)
            # end batch ------------------------------------------------------------------------------------------------

        # Scheduler 学习率衰减
        lr = [x['lr'] for x in optimizer.param_groups]  # for loggers
        scheduler.step() # 根据前面设置的学习率更新策略更新学习率


        if RANK in [-1, 0]:
            # mAP
            callbacks.run('on_train_epoch_end', epoch=epoch)
            ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) # model属性给ema
            final_epoch = (epoch + 1 == epochs) or stopper.possible_stop# 判断当前epoch是否最后一轮
            if not noval or final_epoch:  #
                """
                   测试使用的是ema(指数移动平均 对模型的参数做平均)的模型
                      results: [1] Precision 所有类别的平均precision(最大f1时)
                               [1] Recall 所有类别的平均recall
                               [1] map@0.5 所有类别的平均mAP@0.5
                               [1] map@0.5:0.95 所有类别的平均mAP@0.5:0.95
                               [1] box_loss 验证集回归损失, obj_loss 验证集置信度损失, cls_loss 验证集分类损失
                      maps: [80] 所有类别的mAP@0.5:0.95
                """
                results, maps, _ = val.run(data_dict,
                                           batch_size=batch_size // WORLD_SIZE * 2,# 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
                                           imgsz=imgsz, # 要保证batch_size能整除卡数
                                           model=ema.ema,
                                           single_cls=single_cls,# 是否是单类数据集
                                           dataloader=val_loader,
                                           save_dir=save_dir,# 保存地址 runs/train/expn
                                           plots=False,# 是否可视化
                                           callbacks=callbacks,
                                           compute_loss=compute_loss)# 损失函数(train)

            # Update best mAP
            fi = fitness(np.array(results).reshape(1, -1))  # fi: [P, R, mAP@.5, mAP@.5-.95]的一个加权值 = 0.1*mAP@.5 + 0.9*mAP@.5-.95
            if fi > best_fitness: # 当前fi大于最佳的
                best_fitness = fi # 更新
            log_vals = list(mloss) + list(results) + lr #保存验证结果
            callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) # 保存验证数据
            # *****************************7.保存模型***********************************
            # Save model
            """
               保存带checkpoint的模型用于inference或resuming training
               保存模型, 还保存了epoch, results, optimizer等信息
               optimizer将不会在最后一轮完成后保存
               model保存的是EMA的模型
            """
            if (not nosave) or (final_epoch and not evolve):  # if save
                # 将打钱训练过程中的所有参数赋值给ckpt
                ckpt = {'epoch': epoch,
                        'best_fitness': best_fitness,
                        'model': deepcopy(de_parallel(model)).half(),
                        'ema': deepcopy(ema.ema).half(),
                        'updates': ema.updates,
                        'optimizer': optimizer.state_dict(),
                        'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None,
                        'date': datetime.now().isoformat()}

                # Save last, best and delete
                torch.save(ckpt, last) # 保存每轮的模型
                if best_fitness == fi: # 如果是最佳的模型
                    torch.save(ckpt, best)
                if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0):
                    torch.save(ckpt, w / f'epoch{epoch}.pt') # 保存模型
                del ckpt # 释放内存
                callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) # 保存到日志

            # Stop Single-GPU 停止单卡训练
            if RANK == -1 and stopper(epoch=epoch, fitness=fi):
                break

            # Stop DDP TODO: known issues shttps://github.com/ultralytics/yolov5/pull/4576
            # stop = stopper(epoch=epoch, fitness=fi)
            # if RANK == 0:
            #    dist.broadcast_object_list([stop], 0)  # broadcast 'stop' to all ranks

        # Stop DPP
        # with torch_distributed_zero_first(RANK):
        # if stop:
        #    break  # must break all DDP ranks

        # end epoch ----------------------------------------------------------------------------------------------------
    # end training -----------------------------------------------------------------------------------------------------
    # *****************************8.打印信息并释放显存***********************************
    if RANK in [-1, 0]:
        LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.')# 停止训练,向控制台输出信息
        for f in last, best:
            if f.exists():
                strip_optimizer(f)  # 训练完成后 讲过optimizer从ckpt中删除
                if f is best:
                    LOGGER.info(f'\nValidating {f}...') # 把最好的模型在验证集跑一边 并绘图
                    results, _, _ = val.run(data_dict,
                                            batch_size=batch_size // WORLD_SIZE * 2,
                                            imgsz=imgsz,
                                            model=attempt_load(f, device).half(),
                                            iou_thres=0.65 if is_coco else 0.60,  # best pycocotools results at 0.65
                                            single_cls=single_cls,
                                            dataloader=val_loader,
                                            save_dir=save_dir,
                                            save_json=is_coco,
                                            verbose=True,
                                            plots=True,
                                            callbacks=callbacks,
                                            compute_loss=compute_loss)  # val best model with plots
                    if is_coco: # 如果hi是coco数据集
                        callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi)
        # 记录训练终止时日志
        callbacks.run('on_train_end', last, best, plots, epoch, results)
        LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}")

    torch.cuda.empty_cache() # 释放显存
    return results


# ----------------------------------6 opt---------------------------------
def parse_opt(known=False):
    parser = argparse.ArgumentParser()
    # weights: 预训练权重文件
    parser.add_argument('--weights', type=str, default=ROOT / 'pretrained/yolov5s.pt', help='initial weights path')
    # cfg: 训练模型 模型配置文件 网络结构
    parser.add_argument('--cfg', type=str, default=ROOT / 'models/yolov5s.yaml', help='model.yaml path')
    # data: 训练路径,包括训练集,验证集,测试集的路径,类别总数等
    parser.add_argument('--data', type=str, default=ROOT / 'data/fire_data.yaml', help='dataset.yaml path')
    # hpy: 超参数设置文件(lr/sgd/mixup)./data/hyps/下面有5个超参数设置文件,每个文件的超参数初始值有细微区别,用户可以根据自己的需求选择其中一个
    parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch.yaml', help='hyperparameters path')
    # epochs: 训练轮次, 默认轮次为300次
    parser.add_argument('--epochs', type=int, default=300)
    # batchsize: 训练批次, 默认bs=16
    parser.add_argument('--batch-size', type=int, default=4, help='total batch size for all GPUs, -1 for autobatch')
    # imagesize: 设置图片大小, 默认640*640
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
    # rect: 是否采用矩形训练,默认为False
    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: 不保存模型  默认False(保存)  在./runs/exp*/train/weights/保存两个模型 一个是最后一次的模型 一个是最好的模型
    parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
    # noval: 最后进行测试, 设置了之后就是训练结束都测试一下, 不设置每轮都计算mAP, 建议不设置
    parser.add_argument('--noval', action='store_true', help='only validate final epoch')
    # noautoanchor: 不自动调整anchor, 默认False, 自动调整anchor
    parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
    # evolve: 参数进化, 遗传算法调参
    parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
    # bucket: 谷歌优盘 / 一般用不到
    parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
    # cache: 是否提前缓存图片到内存,以加快训练速度,默认False
    parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"')
    # mage-weights: 使用图片采样策略,默认不使用
    parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
    # device: 设备选择
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    # parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
    # multi-scale 是否进行多尺度训练
    parser.add_argument('--multi-scale', default=True, help='vary img-size +/- 50%%')
    # single-cls: 数据集是否多类/默认True
    parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
    # optimizer: 优化器选择 / 提供了三种优化器
    parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
    # sync-bn: 是否使用跨卡同步BN,在DDP模式使用
    parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
    # dataloader的最大worker数量 (使用多线程加载图片)
    parser.add_argument('--workers', type=int, default=0, help='max dataloader workers (per RANK in DDP mode)')
    # 训练结果的保存路径
    parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
    # 训练结果的文件名称
    parser.add_argument('--name', default='exp', help='save to project/name')
    # 项目位置是否存在 / 默认是都不存在
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    # 四元数据加载器: 允许在较低 --img 尺寸下进行更高 --img 尺寸训练的一些好处。
    parser.add_argument('--quad', action='store_true', help='quad dataloader')
    # cos-lr: 余弦学习率
    parser.add_argument('--linear-lr', action='store_true', help='linear LR')
    # 标签平滑 / 默认不增强, 用户可以根据自己标签的实际情况设置这个参数,建议设置小一点 0.1 / 0.05
    parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
    # 早停止耐心次数 / 100次不更新就停止训练
    parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
    # --freeze冻结训练 可以设置 default = [0] 数据量大的情况下,建议不设置这个参数
    parser.add_argument('--freeze', type=int, default=0, help='Number of layers to freeze. backbone=10, all=24')
    # --save-period 多少个epoch保存一下checkpoint
    parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
    # --local_rank 进程编号 / 多卡使用
    parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
    # Weights & Biases arguments
    # 在线可视化工具,类似于tensorboard工具
    parser.add_argument('--entity', default=None, help='W&B: Entity')
    # upload_dataset: 是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集) 默认False
    parser.add_argument('--upload_dataset', action='store_true', help='W&B: Upload dataset as artifact table')
    # bbox_interval: 设置界框图像记录间隔 Set bounding-box image logging interval for W&B 默认-1   opt.epochs // 10
    parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval')
    # 使用数据的版本
    parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')
    # 作用就是当仅获取到基本设置时,如果运行命令中传入了之后才会获取到的其他配置,不会报错;而是将多出来的部分保存起来,留到后面使用
    opt = parser.parse_known_args()[0] if known else parser.parse_args()
    return opt

# ----------------------------------7. run---------------------------------
def run(**kwargs):
    # Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolov5m.pt')
    # 执行这个脚本/调用train函数/开启训练
    """
        使得支持指令执行脚本 与主函数差不多 为导入时提供
    """
    opt = parse_opt(True)
    for k, v in kwargs.items():
        setattr(opt, k, v) # 属性赋值,属性不存在则创建一个赋值
    main(opt)
# ----------------------------------8. main---------------------------------
def main(opt, callbacks=Callbacks()):
    # ----------------------------------8.1 检查分布式训练环境---------------------------------
    # Checks
    if RANK in [-1, 0]:
        print_args(FILE.stem, opt) # 以彩色方式输出所有训练参数
        check_git_status() # 检查github库是否更新
        check_requirements(exclude=['thop']) # 检查requirement.txt包是否都满足
    # ----------------------------------8.2 判断是否断点续训练---------------------------------
    # Resume
    if opt.resume and not check_wandb_resume(opt) and not opt.evolve:  # resume an interrupted run
        ckpt = opt.resume if isinstance(opt.resume, str) else get_latest_run()  # 找到最近的权重文件last.pt
        assert os.path.isfile(ckpt), 'ERROR: --resume checkpoint does not exist' # 判断是否是文件,若不是则抛出文件异常
        with open(Path(ckpt).parent.parent / 'opt.yaml', errors='ignore') as f: # opt.yaml是训练时的命令行参数问及那
            opt = argparse.Namespace(**yaml.safe_load(f))  # 超参数替换,将训练时的命令行参数加载到opt参数对象中
        opt.cfg, opt.weights, opt.resume = '', ckpt, True  # 设置为‘’ 对应train函数里面的操作 即加载权重时是否加载anchor
        LOGGER.info(f'Resuming training from {ckpt}') # 打印从ckpt恢复断点训练信息
    else:
        # 不适用断点续训,从文件中读取相关参数
        opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \
            check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project)  # checks
        assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' #如果模型文件和权重文件为空 弹出警告
        if opt.evolve:
            opt.project = str(ROOT / 'runs/evolve') # 设置心得项目输出目录
            opt.exist_ok, opt.resume = opt.resume, False  # 将resume传递给exiist_ok
        opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) #根据opt.project生成目睹并给opt.save_dir

    # ----------------------------------8.3 判断是否分布式训练---------------------------------
    """
        DDP用于单机或多级的多GPU分布式训练,但目前DDP只能在linux使用,
    """
    # DDP mode
    device = select_device(opt.device, batch_size=opt.batch_size) # 选择程序装载的位置
    if LOCAL_RANK != -1: # 当进程内的GPU!=-1 进度DDP
        assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' # 用于DDP训练GPU数量不足
        assert opt.batch_size % WORLD_SIZE == 0, '--batch-size must be multiple of CUDA device count' # world_size表示全局进程数
        assert not opt.image_weights, '--image-weights argument is not compatible with DDP training' # 不能使用图片采样策略
        assert not opt.evolve, '--evolve argument is not compatible with DDP training' # 不能使用超参数进化
        torch.cuda.set_device(LOCAL_RANK) # 设置装载程序设备
        device = torch.device('cuda', LOCAL_RANK) # 保存装载程序谁不
        dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo") # 用于多GPU训练模块

    # ----------------------------------8.4 判断是否超参数进化训练---------------------------------
    """
        遗传算法,每次保存最优结果,但至少需要300次
            1.若存在evolve.csv 读取文件中训练数据,选择超参进化方式,结果最优的训练数据突变超参数
            2.限制超参进化参数hyp在规定范围内
            3.使用突变后的超参数进行训练,测试其结果
            4.训练结束后,将训练可视化并保存在evolution.csv
        遗传算法4个参数:群体大学 进化次数 交叉概率 变异概率
    """
    # Train
    if not opt.evolve:  #判断是否进行进化
        train(opt.hyp, opt, device, callbacks) # 直接从train开始
        if WORLD_SIZE > 1 and RANK == 0: # 如果全局进程大于1并且rank=0
            LOGGER.info('Destroying process group... ') # 日志输出,摧毁进程组
            dist.destroy_process_group() # 训练完毕,销毁所有进程

    # Evolve hyperparameters (optional)
    else:
        # 超参数列表范围
        meta = {'lr0': (1, 1e-5, 1e-1),  # initial learning rate (SGD=1E-2, Adam=1E-3)
                'lrf': (1, 0.01, 1.0),  # final OneCycleLR learning rate (lr0 * lrf)
                'momentum': (0.3, 0.6, 0.98),  # SGD momentum/Adam beta1
                'weight_decay': (1, 0.0, 0.001),  # optimizer weight decay
                'warmup_epochs': (1, 0.0, 5.0),  # warmup epochs (fractions ok)
                'warmup_momentum': (1, 0.0, 0.95),  # warmup initial momentum
                'warmup_bias_lr': (1, 0.0, 0.2),  # warmup initial bias lr
                'box': (1, 0.02, 0.2),  # box loss gain
                'cls': (1, 0.2, 4.0),  # cls loss gain
                'cls_pw': (1, 0.5, 2.0),  # cls BCELoss positive_weight
                'obj': (1, 0.2, 4.0),  # obj loss gain (scale with pixels)
                'obj_pw': (1, 0.5, 2.0),  # obj BCELoss positive_weight
                'iou_t': (0, 0.1, 0.7),  # IoU training threshold
                'anchor_t': (1, 2.0, 8.0),  # anchor-multiple threshold
                'anchors': (2, 2.0, 10.0),  # anchors per output grid (0 to ignore)
                'fl_gamma': (0, 0.0, 2.0),  # focal loss gamma (efficientDet default gamma=1.5)
                'hsv_h': (1, 0.0, 0.1),  # image HSV-Hue augmentation (fraction)
                'hsv_s': (1, 0.0, 0.9),  # image HSV-Saturation augmentation (fraction)
                'hsv_v': (1, 0.0, 0.9),  # image HSV-Value augmentation (fraction)
                'degrees': (1, 0.0, 45.0),  # image rotation (+/- deg)
                'translate': (1, 0.0, 0.9),  # image translation (+/- fraction)
                'scale': (1, 0.0, 0.9),  # image scale (+/- gain)
                'shear': (1, 0.0, 10.0),  # image shear (+/- deg)
                'perspective': (0, 0.0, 0.001),  # image perspective (+/- fraction), range 0-0.001
                'flipud': (1, 0.0, 1.0),  # image flip up-down (probability)
                'fliplr': (0, 0.0, 1.0),  # image flip left-right (probability)
                'mosaic': (1, 0.0, 1.0),  # image mixup (probability)
                'mixup': (1, 0.0, 1.0),  # image mixup (probability)
                'copy_paste': (1, 0.0, 1.0)}  # segment copy-paste (probability)

        with open(opt.hyp, errors='ignore') as f:
            hyp = yaml.safe_load(f)  # load hyps dict
            if 'anchors' not in hyp:  # 如果超参数中没有anchor 则设置为3
                hyp['anchors'] = 3
        opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir)  # 使用遗传算法时,仅在最后的epoch测试和保存
        # ei = [isinstance(x, (int, float)) for x in hyp.values()]  # evolvable indices
        evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv'
        if opt.bucket:
            os.system(f'gsutil cp gs://{opt.bucket}/evolve.csv {save_dir}')  # download evolve.csv if exists
        # 迭代次数 默认300
        for _ in range(opt.evolve):  # generations to evolve
            if evolve_csv.exists():  # if evolve.csv 存在
                # Select parent(s)
                parent = 'single'  # 选择进化方式 有: 'single' or 'weighted'
                x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1) # j加载 evolve.txt
                n = min(5, len(x))  # 选取至多前五次进化结果
                x = x[np.argsort(-fitness(x))][:n]  # argsort从小到大排序 fitness为x前4项(p r mAP mAP:0.95)加权
                w = fitness(x) - fitness(x).min() + 1E-6  # 根据mp mr mAP50 mAP的加权和作为权重计算hyp权重
                if parent == 'single' or len(x) == 1: # 根据不同进化方式获得base hyp
                    # x = x[random.randint(0, n - 1)]  # random selection
                    x = x[random.choices(range(n), weights=w)[0]]  # 根据权重的几率随机挑选适应历史前5的其中一个
                elif parent == 'weighted':
                    x = (x * w.reshape(n, 1)).sum(0) / w.sum()  # 对hyp乘上对应的权重融合曾一个hpy 再取平均 /权重和

                # Mutate 突变 超参数进化
                mp, s = 0.8, 0.2  # 设置图片概率
                npr = np.random
                npr.seed(int(time.time())) # 根据时间设置随机数种子 伪随机
                g = np.array([meta[k][0] for k in hyp.keys()])  # 获取突变初始值(meta三个值中的第一个) meta=[变异初始概率,最低限制,最大限制]
                ng = len(meta)
                v = np.ones(ng) # 确保其中至少有一个参数超变异了
                while all(v == 1):  # 设置突变
                    v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
                for i, k in enumerate(hyp.keys()):  # 将突变添加到base hyp上
                    hyp[k] = float(x[i + 7] * v[i])  # mutate

            # Constrain to limits 限制hpy在规定范围内
            for k, v in meta.items():
                # hyp是超参数配置文件对象 kv是在元超参数中遍历出来的(是元组),hyp的v是一个数
                hyp[k] = max(hyp[k], v[1])  # 限定最小值 选择二者之间大的,防止hyp值过小
                hyp[k] = min(hyp[k], v[2])  # 限定最大值 选二者之间最小的
                hyp[k] = round(hyp[k], 5)  # 四舍五入到小数点后五位
                # 最后得到的值是hyo中的值与meta的最大值之间小的

            # Train mutation 使用突变后的超参数 测试结果
            results = train(hyp.copy(), opt, device, callbacks)

            # Write mutation results
            # 将结果写入到results中,并将byp写入到evolve.txt(每一行为一次进化结果)
            # 其中evolve.txt每一行为(p,r,mAP.F1,test_losses(GIOU,obj,cls))之后为hyp 最后将hyp保存到yaml文件中
            print_mutation(results, hyp.copy(), save_dir, opt.bucket)

        # Plot results 结果可视化
        plot_evolve(evolve_csv)
        LOGGER.info(f'Hyperparameter evolution finished\n'
                    f"Results saved to {colorstr('bold', save_dir)}\n"
                    f'Use best hyperparameters example: $ python train.py --hyp {evolve_yaml}')




# python train.py --data mask_data.yaml --cfg mask_yolov5s.yaml --weights pretrained/yolov5s.pt --epoch 100 --batch-size 4 --device cpu
# python train.py --data mask_data.yaml --cfg mask_yolov5l.yaml --weights pretrained/yolov5l.pt --epoch 100 --batch-size 4
# python train.py --data mask_data.yaml --cfg mask_yolov5m.yaml --weights pretrained/yolov5m.pt --epoch 100 --batch-size 4
if __name__ == "__main__":
    opt = parse_opt()
    main(opt)

        当我们学习完train.py后,就可以训练自己的数据集了,通常我们训练完数据后需要通过val(test).py取进行测试验证当前模型的mAP等超参数是否最佳,如果不是最佳则调整train直至最佳,最终通过detect.py取泛化使用。所以下一期要学习的就是val.py啦!

        如果内容对你有所帮助,不要忘记随手点个赞呀!

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值