train.py
目录
2.def train(hyp, opt, device, callbacks):
4.def main(opt, callbacks=Callbacks()):
1.所需的库和模块
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Train a YOLOv5 model on a custom dataset.
在自定义数据集上训练 YOLOv5 模型。
Models and datasets download automatically from the latest YOLOv5 release.
模型和数据集从最新的 YOLOv5 版本自动下载。
Usage - Single-GPU training:
$ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended)
$ python train.py --data coco128.yaml --weights '' --cfg yolov5s.yaml --img 640 # from scratch
Usage - Multi-GPU DDP training:
$ python -m torch.distributed.run --nproc_per_node 4 --master_port 1 train.py --data coco128.yaml --weights yolov5s.pt --img 640 --device 0,1,2,3
Models: https://github.com/ultralytics/yolov5/tree/master/models
Datasets: https://github.com/ultralytics/yolov5/tree/master/data
Tutorial: https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data
"""
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.optim import lr_scheduler # tensorboard模块
from tqdm import tqdm # 进度条模块
'''获取当前文件和根目录:FILE是当前脚本文件的绝对路径,ROOT是YOLOv5的根目录。如果根目录不在系统路径中,就将其添加到系统路径中,方便后续引用。'''
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
import val as validate # for end-of-epoch mAP
from models.experimental import attempt_load
from models.yolo import Model
from utils.autoanchor import check_anchors
from utils.autobatch import check_train_batch_size
from utils.callbacks import Callbacks
from utils.dataloaders import create_dataloader
from utils.downloads import attempt_download, is_url
from utils.general import (LOGGER, TQDM_BAR_FORMAT, check_amp, check_dataset, check_file, check_git_info,
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,
yaml_save)
from utils.loggers import Loggers
from utils.loggers.comet.comet_utils import check_comet_resume
from utils.loss import ComputeLoss
from utils.metrics import fitness
from utils.plots import plot_evolve
from utils.torch_utils import (EarlyStopping, ModelEMA, de_parallel, select_device, smart_DDP, smart_optimizer,
smart_resume, torch_distributed_zero_first)
# os.getenv(key, default = None) 此方法返回一个字符串,该字符串表示环境变量键的值。如果 key 不存在,则返回默认参数的值。
# key:表示环境变量名称的字符串
# default (可选):表示 key 不存在时默认值的字符串。如果省略,则默认设置为“无”。
# local_rank :进程内 GPU 编号,非显式参数,由 torch.distributed.launch 内部指定。比方说, rank=3,local_rank=0 表示第 3 个进程内的第 1 块 GPU
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
# rank :表示进程序号,用于进程间通信,可以用于表示进程的优先级。我们一般设置 rank=0 的主机为 master 节点。
RANK = int(os.getenv('RANK', -1))
# world_size :全局进程个数。
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))
# check_git_info() -> YOLOv5 git info 检查,返回 {remote, branch, commit}
GIT_INFO = check_git_info()
2.def train(hyp, opt, device, callbacks):
# opt参数是 parse_opt() 方法中设置的参数值
# callbacks = Callbacks() callbacks是Callbacks()类的对象 class Callbacks: -> 处理 YOLOv5 Hooks 的所有已注册回调
def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary 超参数( hyp )是 path/to/hyp.yaml 或超参数( hyp )字典
'''
函数定义:
接受四个参数:
hyp :表示超参数,可能是超参数的 YAML 文件路径或者一个字典。
opt :包含训练选项的对象,如保存路径、训练的周期数、批次大小等等。
device :指定用于训练的设备(例如,CPU 或 CUDA 设备)。
callbacks :一个回调对象,用于在训练过程中的特定时刻执行特定操作。
'''
# epochs :总训练次数
# batch_size :所有 GPU 的总批量大小,自动批处理为 -1
# weights :初始权重路径
# single_cls :将多类数据训练为单类
# evolve :进化 x 代的超参数
# data :数据集.yaml 路径
# cfg :模型.yaml path
# resume :恢复最近的训练
# noval :仅验证最后一个世代(epoch)
# nosave :仅保存最终检查点
# workers :最大数据加载器工作者(DDP 模式下每个 RANK)
# freeze :冻结层:backbone=10,first3=0 1 2
'''
参数解包:
这一行将 opt 对象中的多个属性提取出来,赋值给对应的变量,如下:
save_dir:保存模型的目录,通过 Path 模块来处理路径。
epochs:训练的总周期数。
batch_size:每次迭代的批次大小。
weights:预训练模型的权重路径。
single_cls:是否将多类数据当作单类数据进行训练的标志。
evolve:是否进行超参数演化的标志。
data:数据集的路径。
cfg:模型配置文件的路径。
resume:是否从上次保存的状态恢复训练的标志。
noval:是否只在最后一轮进行验证的标志。
nosave:是否只保存最终的检查点的标志。
workers:数据加载的工作线程数。
freeze:需要冻结的层的列表。
'''
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
# callbacks.run() -> 循环遍历已注册的操作并在主线程上触发所有回调
'''
回调函数:
这行代码调用回调对象的 run 方法,传入事件名称 'on_pretrain_routine_start'。这常常用于在训练开始之前执行某些操作,比如记录日志或进行设定。
'''
callbacks.run('on_pretrain_routine_start') # 在预训练例程开始时
'''创建训练权重目录和保存路径'''
# Directories 路径
w = save_dir / 'weights' # weights dir 权重路径
'''
创建目录:
mkdir(parents=True, exist_ok=True) 的作用是:
parents=True 表示如果上层目录不存在,则一并创建。
exist_ok=True 表示如果目录已经存在,不会抛出错误,这样可以安全地运行而不担心目录已经存在的问题。
'''
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir 制作路径
last, best = w / 'last.pt', w / 'best.pt'
# Hyperparameters 超参数
'''检查变量 hyp 是否是字符串类型。如果是字符串,通常意味着 hyp 是一个文件路径,包含超参数的定义。'''
if isinstance(hyp, str):
'''
如果 hyp 是一个字符串,则打开这个字符串指定的文件(假设该文件是 YAML 格式的),并使用 yaml.safe_load 函数将其内容解析为一个字典。这个字典包含了超参数的具体值,例如学习率、权重衰减等。
'''
with open(hyp, errors='ignore') as f:
hyp = yaml.safe_load(f) # load hyps dict 加载超参数(hyps)字典
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
'''保存超参数副本:最后,将读取到的超参数字典做一个拷贝,保存到 opt 对象的 hyp 属性中。这种做法方便后续将这些超参数保存在检查点(checkpoints)中,以便于后续实验或调试使用。'''
opt.hyp = hyp.copy() # for saving hyps to checkpoints 用于保存 hyp 到检查点
# Save run settings 保存运行设置
if not evolve:
# yaml_save() -> 单行安全 yaml 保存
'''
这里定义了两个文件路径:
last 将会保存为最新的训练权重,文件名叫 last.pt。
best 将会保存训练过程中表现最好的权重,文件名叫 best.pt。
这两个文件都保存在之前创建的 weights 目录下。
'''
yaml_save(save_dir / 'hyp.yaml', hyp)
yaml_save(save_dir / 'opt.yaml', vars(opt))
'''
这一代码片段的主要功能是保存训练运行的超参数和选项设置。它通过条件判断确定是否处于超参数进化阶段,仅在非进化模式下将参数保存为 YAML 格式的文件。
这种做法确保了当前训练的配置能够被记录和复用,提升了实验的可重复性和可追溯性。
'''
# Loggers 记录器
'''设置日志记录器'''
'''这里初始化了一个变量data_dict,用于在后续处理中存储数据字典的引用。起初将其设置为None。'''
data_dict = None
'''仅在分布式训练时的主节点(RANK为-1或0)执行下面的操作。这是因为只有主节点负责处理日志记录。'''
if RANK in {-1, 0}:
'''
创建了一个Loggers类的实例,传入了一些参数:
save_dir: 保存日志的目录。
weights: 模型权重。
opt: 训练选项。
hyp: 超参数。
LOGGER: 日志记录器实例。
loggers实例将用于记录训练的相关信息,例如损失、准确率等。
'''
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance 记录器实例
# Register actions 注册操作
'''
注册回调操作
通过methods(loggers)获取loggers实例中所有可用的方法,并为每个方法在callbacks对象中注册一个操作。
getattr(loggers, k)调用相应的方法以便在有事件触发时可以执行。这样可以实现训练过程中的自动日志记录。
'''
for k in methods(loggers):
# callbacks.register_action() -> 向回调钩子注册一个新动作
callbacks.register_action(k, callback=getattr(loggers, k))
# Process custom dataset artifact link 处理自定义数据集工件链接
# loggers.remote_dataset() -> 如果提供了自定义数据集工件链接,则获取 data_dict
'''
处理数据集的远程链接
这里从loggers实例中获取远程数据集的链接,并将其赋值给data_dict。这可能用于保存训练与验证数据的引用。
'''
data_dict = loggers.remote_dataset
'''
如果在恢复训练,更新相关参数
如果设置了resume,说明正在从之前的训练状态中恢复。这时,将当前的weights、epochs、hyp和batch_size参数更新为训练选项中的值,以确保在恢复训练时使用的参数是最新的。
'''
if resume: # If resuming runs from remote artifact 如果从远程工件恢复运行
weights, epochs, hyp, batch_size = opt.weights, opt.epochs, opt.hyp, opt.batch_size
# Config 配置
'''该行代码用来判断是否需要生成训练过程的绘图。当不处于超参数进化(evolve 为 True)且 opt.noplots 为 False 时,变量 plots 将被设为 True,表示生成绘图;反之,则设为 False。'''
plots = not evolve and not opt.noplots # create plots 创建图
'''这里判断当前使用的设备是否为 GPU。如果 device.type 不是 'cpu',则 cuda 变量将被设为 True,表示模型将在 GPU 上训练;否则,为 False,表示使用 CPU。'''
cuda = device.type != 'cpu'
# init_seeds() -> 初始化随机数生成器(RNG)种子
'''此行调用 init_seeds 函数来初始化随机种子,以确保实验的可重现性。使用的种子是给定的 opt.seed 加上 1 和当前进程的编号 RANK,并且设置为确定性计算(deterministic=True),使得每次运行结果一致。'''
init_seeds(opt.seed + 1 + RANK, deterministic=True)
# torch_distributed_zero_first() -> 同步所有的进程,直到整组(也就是所有节点的所有GPU)到达这个函数的时候,才会执行后面的代码
'''这个上下文管理器用于控制在分布式训练中,保证特定任务的执行顺序。 LOCAL_RANK 用于识别当前设备的序号,这样可以避免不同设备间的竞态条件。'''
with torch_distributed_zero_first(LOCAL_RANK):
# check_dataset() -> 如果本地未找到数据集,请下载、检查和/或解压缩数据集
'''这一行代码检查 data_dict 是否为 None。如果是,则调用 check_dataset(data) 函数来验证和获取数据集信息,并赋值给 data_dict。这样确保训练时有有效的数据集信息。'''
data_dict = data_dict or check_dataset(data) # check if None 检查是否无
'''从 data_dict 中获取训练集和验证集的路径,分别赋值给 train_path 和 val_path。这两个路径将用于后续的数据加载。'''
train_path, val_path = data_dict['train'], data_dict['val']
'''根据是否是单类训练(single_cls),确定类别数量(nc)。若是单类,则 nc 设置为 1;若为多类,从 data_dict 中读取类别数量。'''
nc = 1 if single_cls else int(data_dict['nc']) # number of classes 类别数量
'''此行根据是否是单类训练来定义类别名称。如果是单类并且 data_dict['names'] 的长度不为1,则将名称设置为默认的 0: 'item';否则,直接使用 data_dict['names'] 来获取所有类别名称。'''
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names 类别名称
'''该行代码检查验证集的路径 val_path 是否为字符串类型,并且是否以 'coco/val2017.txt' 结尾,从而判断当前的数据集是否为 COCO 数据集。'''
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset COCO 数据集
'''
总之
整个初始化步骤主要解析各种yaml的参数 +
创建训练权重目录和保存路径 +
读取超参数配置文件 +
设置保存参数保存路径 +
加载数据配置信息 +
加载日志信息(logger + wandb) +
加载其他参数(plots、cuda、nc、names、is_coco)
'''
'''初始化网络模型'''
# Model 模型
# check_suffix() -> 检查文件是否有可接受的后缀
'''检查权重文件后缀:这行代码用于检查weights变量中指定的文件名是否以.pt结尾,以确保它是一个有效的PyTorch权重文件格式。'''
check_suffix(weights, '.pt') # check weights 检查权重
'''确定是否使用预训练权重:这里,pretrained变量被设置为一个布尔值,指示权重文件是否为预训练模型的权重。'''
pretrained = weights.endswith('.pt')
if pretrained:
'''
如果是预训练权重:
如果pretrained为True,那么代码进入此块。torch_distributed_zero_first(LOCAL_RANK)是一个上下文管理器,确保在分布式训练的某些场景下,只有一个进程会尝试下载权重。
attempt_download(weights)函数尝试从远程位置下载指定的权重文件,如果本地不存在该文件。
'''
# torch_distributed_zero_first() -> 同步所有的进程,直到整组(也就是所有节点的所有GPU)到达这个函数的时候,才会执行后面的代码
with torch_distributed_zero_first(LOCAL_RANK):
# attempt_download() -> 如果本地找不到,则尝试从 GitHub 发布资产下载文件。release = 'latest'、'v7.0' 等。
weights = attempt_download(weights) # download if not found locally 如果本地未找到则下载
'''加载检查点:使用torch.load函数加载权重文件,并将其指定为在CPU上加载,以防止潜在的CUDA内存泄露。'''
ckpt = torch.load(weights, map_location='cpu') # load checkpoint to CPU to avoid CUDA memory leak 将检查点加载到 CPU 以避免 CUDA 内存泄漏
# Model = DetectionModel # retain YOLOv5 'Model' class for backwards compatibility 保留 YOLOv5“模型”类以实现向后兼容
'''创建模型:根据配置文件cfg或从加载的检查点中提取的YAML文件创建一个新的模型实例。ch=3表示输入通道数,nc是类别数量,anchors则由超参数字典hyp中的锚点获取。模型最后被移动到指定的设备(如CUDA或CPU)。'''
model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create 创建
'''
处理从检查点加载的状态字典:
exclude用于指定在加载模型状态字典时需要排除的键。
随后,通过将检查点中的状态字典转换为浮点格式并与模型的当前状态字典进行交集检查,以确保只加载兼容的部分。
最后,加载这些状态字典到模型中, strict=False 允许不严格地匹配键。
'''
exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys 排除键
# pytorch 中的 state_dict() 是一个简单的python的字典对象,将每一层与它的对应参数建立映射关系.(如model的每一层的weights及偏置等等)。
# 注意,只有那些参数可以训练的layer才会被保存到模型的state_dict中,如卷积层,线性层等等。
# 优化器对象Optimizer也有一个state_dict,它包含了优化器的状态以及被使用的超参数(如lr, momentum,weight_decay等)。
# 备注:
# state_dict是在定义了model或optimizer之后pytorch自动生成的,可以直接调用.常用的保存state_dict的格式是".pt"或’.pth’的文件,即下面命令的 PATH="./***.pt"
# torch.save(model.state_dict(), PATH)
# load_state_dict 也是model或optimizer之后pytorch自动具备的函数,可以直接调用
# model = TheModelClass(*args, **kwargs)
# model.load_state_dict(torch.load(PATH))
# model.eval()
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 检查点 state_dict 作为 FP32
# intersect_dicts() -> 匹配键和形状的字典交集,省略“exclude”键,使用 csd 值
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect 相交
# 在深度学习中,模型的训练是一个长期且资源消耗巨大的过程。为了能够在不同环境或时间点之间方便地共享和复用模型,我们通常需要将模型的状态保存下来。
# 而load_state_dict()函数就是PyTorch中用于加载模型状态字典的重要工具。
# load_state_dict()函数的作用是将之前保存的模型参数加载到当前模型的实例中,从而恢复模型的训练状态。这对于模型的部署、迁移学习以及持续训练等场景都至关重要。
model.load_state_dict(csd, strict=False) # load 加载
# 从 {weights} 转移了 {len(csd)}/{len(model.state_dict())} 个项目
'''记录加载信息:这行代码记录了成功加载的参数数量,以便在训练日志中提供反馈。'''
LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report 报告
else:
'''如果不是预训练权重:如果输入的weights不是预训练模型权重,则会直接根据指定的配置cfg创建一个新的模型实例。'''
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
# check_amp() -> 检查 PyTorch 自动混合精度 (AMP) 功能。操作正确则返回 True
'''检查是否支持自动混合精度(AMP):调用check_amp(model)以检查模型是否支持自动混合精度,这对于加速训练过程和减少内存使用是有帮助的。'''
amp = check_amp(model) # check AMP 检查 AMP
'''
这里使用预训练权重参数,是类似于迁移学习。
预训练的模型是检测coco数据集的模型,数据集中有80个类别,而自己的训练集类别以及类别的数量,并不与coco数据集相同。
所以要先加载一个新的模型,把预训练的参数加载至模型作为初始参数,再把识别的类别改成自己的数据集要识别的类别。接下来将预训练参数中与新模型中相同的参数加载至模型。
'''
''' 冻结模型中的某些层 这段代码的目的是在训练过程中冻结某些网络层的参数,以便在训练时不更新这些层的权重。'''
# Freeze 冻结
'''
定义冻结的层:
这里的 freeze 是一个列表推导式,根据原始的 freeze 列表(可能是数字或字符串)的内容生成一个新的列表。
如果 freeze 的长度大于 1,则遍历这个列表;否则,将其转换为一个范围,从 0 到 freeze[0],表示需要冻结的层的索引(通常是由外部参数指定的)。
结果是 freeze 包含了需要冻结的层标识符,例如 model.0., model.1.等。
'''
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze 冻结层
# named_parameters() 是 PyTorch 中一个非常有用的函数,用于访问模型中所有定义的参数及其对应的名称。
# 它是 torch.nn.Module 类的方法之一,返回一个生成器,生成 (name, parameter) 对,name 是参数的名称,parameter 是对应的参数张量。
'''迭代模型中的参数:遍历模型的所有参数,k 是参数的名称,v 是参数的值(即Tensor)。named_parameters 返回一个包含模型所有参数的可迭代对象。'''
for k, v in model.named_parameters():
# requires_grad 表达的含义是,这一参数是否保留(或者说持有,即在前向传播完成后,是否在显存中记录这一参数的梯度,而非立即释放)梯度,等待优化器执行optim.step()更新参数。
# 1. 当requires_grad = False,则不保留梯度,因此即便在optimizer中注册了参数,也没有梯度可以用来更新参数,因此参数不变。
# 不过不影响梯度继续反向传播,即假设某一层(例如第三层)参数的requires_grad为False或True,前面层(第1或2层)参数的梯度都不变。
# 2. 当requires_grad = True,则在前向计算后保留梯度,用于optimizer更新参数。但如果没有在optimizer中注册参数,那么即便保存了梯度也无法更新参数。
'''允许训练所有层:默认情况下,所有层的 requires_grad 属性都设置为 True,这表示在反向传播时都会计算这些参数的梯度。'''
v.requires_grad = True # train all layers 训练所有层
# v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
'''
冻结特定的层:
通过条件判断,检查参数的名称 k 是否在需要冻结的层列表中。
如果是,记录日志,显示正在冻结的层的名称 k,然后将 requires_grad 属性设置为 False,从而阻止这些层的权重在训练过程中被更新。
'''
if any(x in k for x in freeze):
LOGGER.info(f'freezing {k}') # 冻结 {k}
v.requires_grad = False # requires_grad 设置为 False 表示冻结该层
'''
这段代码的主要功能是允许用户选择性地冻结模型中的某些层,以便在训练时不对这些层的参数进行更新。
这通常用于迁移学习中,用户可能希望保留预训练模型的某些层权重,而只训练特定的层。
冻结层通过设置 requires_grad = False 来实现,使得这些层在梯度计算中被忽略,从而提高训练效率并减少过拟合的风险。代码中也包含了日志记录功能,以便跟踪哪些层被冻结。
冻结层的原理是通过设置每个层参数中的requires_grad属性实现的。
若require_grad为True,在反向传播时就会求出此tensor的梯度。
若require_grad为False,则不会求该tensor的梯度。冻结就是通过对某些层不求梯度实现的。默认不进行参数冻结。
'''
'''检查图像尺寸'''
# Image size 图片大小
'''
计算网格尺寸 gs,它是模型的最大步幅(stride)和32中的较大值。
model.stride.max():获取模型检测层的最大步幅。这表示在图像上进行下采样的最大比例,通常影响特征图的尺寸,这对于YOLO(You Only Look Once)模型非常重要,因为它需要在不同的尺度上处理对象。
int(...):将步幅值转换为整数。
max(..., 32):确保即使在步幅较小的情况下,gs 也至少为32。这是为了避免在训练过程中出现过小的网格大小,可能会影响模型的性能。
'''
gs = max(int(model.stride.max()), 32) # grid size (max stride) 网格大小(最大步幅)
# check_img_size() -> 验证图像大小是否是每个维度的 stride 的倍数 -> new_size
'''
验证输入的图像大小 imgsz 是否是 gs 的倍数,并可能将其调整为适合的值。
check_img_size(opt.imgsz, gs, floor=gs * 2):此函数会检查传入的 opt.imgsz(用户指定的图像大小)是否能被 gs 整除。如果不能,它会根据 gs 的值进行调整,以确保图像大小符合网络的要求。
floor=gs * 2 的参数意味着,如果需要调整,所获得的图像大小不应小于 gs * 2,即保证图像有足够的分辨率。
'''
imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2) # verify imgsz is gs-multiple 验证 imgsz 是否为 gs-multiple
'''
这段代码的主要功能是确定合适的网络输入网格大小和图像大小。
首先,通过获取模型的最大步幅来计算网格大小,确保其不小于32。然后,通过验证用户指定的图像大小是否符合模型要求,以保证训练过程中的有效性和稳定性。
'''
'''动态设置批处理 根据当前设备的状态动态估算最佳的训练批量大小(batch size)'''
# Batch size 批量大小
'''
RANK 是一个用于指示当前设备在分布式训练中的排名变量(rank),当 RANK 为 -1 时,表示当前代码在单个 GPU 上运行。
batch_size 是指批量大小。-1 表示用户未指定批量大小,因此代码需要在没有用户输入的情况下做出估计。
'''
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size 仅限单 GPU,估计最佳批量大小
# check_train_batch_size() -> 检查 YOLOv5 训练批次大小 -> 计算最佳批量大小
'''
动态估算最佳批量大小:
这里调用了 check_train_batch_size 函数来估算一个合适的批量大小。
model 参数是当前的 YOLOv5 模型,imgsz 是指定的训练图像尺寸,amp 指是否使用自动混合精度(Automatic Mixed Precision)来加速训练。
函数会根据模型的结构、GPU的内存、图像尺寸等信息来返回一个适合的批量大小,从而保证在训练时不会超出 GPU 内存限制。
'''
batch_size = check_train_batch_size(model, imgsz, amp)
# on_params_update() -> 更新实验的超参数或配置
loggers.on_params_update({"batch_size": batch_size})
'''设置优化器'''
# Optimizer 优化器
'''这行代码定义了一个名为nbs(nominal batch size,名义批量大小)的变量,并将其设置为64。这通常是在训练过程中希望的标准批量大小。'''
nbs = 64 # nominal batch size 模型批量打下
'''
这行代码计算了一个名为accumulate的变量。
nbs / batch_size:这里计算的是“名义批量大小”与实际批量大小的比值。如果实际批量大小batch_size比较小,这个值就会大于1,因此需要被“累积”多次来达到相同的训练效果。
round(...)函数对计算的比值进行四舍五入,确保得到一个整数。
max(..., 1)确保即使batch_size非常大(大于nbs),accumulate的值至少为1。这意味着如果batch_size更小,累积次数会增加。
'''
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing 在优化之前累积损失
'''
这行代码调整损失函数中权重衰减(weight decay)的值。
batch_size * accumulate / nbs 计算了一个比例,这个比例用于调整weight_decay。它的目的是适应当前的批量大小和名义批量大小,使模型在不同的环境下保持训练的稳定性。
通过*=来更新hyp['weight_decay']`的值,这确保了模型在训练中的正则化是适应当前配置的。
'''
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay 规模 weight_decay
# smart_optimizer() -> YOLOv5 3 参数组优化器:0)权重衰减,1)权重不衰减,2)偏差不衰减 -> optimizer
'''
这行代码初始化优化器。
smart_optimizer是一个函数,其中传入的参数包括:
model: 当前正在训练的模型。
opt.optimizer: 指定的优化器类型(如SGD、Adam等)。
hyp['lr0']: 初始学习率。
hyp['momentum']: 动量值(如果使用的优化器支持)。
hyp['weight_decay']: 调整后的权重衰减值。
这个函数将返回一个配置好的优化器。
'''
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay'])
'''
这段代码的主要目的是为神经网络训练过程设置和初始化优化器。
它首先定义了名义批量大小,然后根据实际批量大小计算累积损失的次数,以适应训练的稳定性,并调整权重衰减的值,以便在不同的批量大小下实现一致的正则化效果。
最后,它调用smart_optimizer函数来创建一个优化器,准备在模型训练过程中使用。这一切的目的是优化模型的训练过程,以提高性能和稳定性。
这段代码是参数设置(nbs、accumulate、hyp[‘weight_decay’])
nbs指的是nominal batch size,名义上的batch_size。这里的nbs跟命令行参数中的batch_size不同,命令行中的batch_size默认为16,nbs设置为64。
accumulate 为累计次数,在这里 nbs/batch_size(64/16)计算出 opt.batch_size输入多少批才达到nbs的水平。
简单来说,nbs为64,代表想要达到的batch_size,这里的数值是64;batch_size为opt.batch_size,这里的数值是16。
64/16等于4,也就是opt.batch_size需要输入4批才能达到nbs,accumulate等于4。(round表示四舍五入取整数,而max表示accumulate不能低于1。)
当给模型输入了4批图片数据后,将四批图片数据得到的梯度值,做累积。当每累积到4批数据时,才会对参数做更新,这样就实现了与batch_size=64时相同的效果。
最后还要做权重参数的缩放,因为batch_size发生了变化,所有权重参数也要做相应的缩放。
'''
''''设置学习率 这段代码主要涉及学习率调度器(scheduler)的设置,目的是在模型训练过程中动态调整学习率。'''
# Scheduler 调度器
'''判断是否使用余弦学习率调度(cos_lr)'''
if opt.cos_lr:
# one_cycle() -> 从 y1 到 y2 的正弦斜坡的 lambda 函数 https://arxiv.org/pdf/1812.01187.pdf
'''
定义余弦调度学习率函数:
如果使用余弦调度,调用 one_cycle 函数来生成一个学习率变化的函数。这个函数用于在训练过程中根据训练的进度(epoch)调整学习率。
1 表示训练开始时的学习率比例。
hyp['lrf'] 是最终学习率的比例。
epochs 是训练的总轮数。
'''
lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf'] 余弦 1->hyp['lrf']
else:
'''
定义线性学习率调度函数:
如果不使用余弦调度,定义一个线性学习率调整函数 lf。这个 lambda 函数根据当前的 epoch 的进度线性地调整学习率:
x 是当前的 epoch。
(1 - x / epochs) 逐渐减少,表示随着训练的进行,学习率开始减小。
hyp['lrf'] 是设置的最小学习率,确保学习率不会降到某个阈值以下。
'''
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf'] # linear 线性
# torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1, verbose=False)
# 在python中,有个东西叫做匿名函数(lambda表达式),能够用于很方便的定义各种规则,这个 LambdaLR() 也就可以理解成自定义规则去调整网络的学习率。
# 从另一个角度理解,数学中的 λ 一般是作为系数使用,因此这个学习率调度器的作用就是 将初始学习率乘以人工规则所生成的系数λ。
# 参数:
# optimizer :被调整学习率的优化器。
# lr_lambda :用户自定义的学习率调整规则。可以是lambda表达式,也可以是函数。
# last_epoch :当前优化器的已迭代次数,后文我们将其称为epoch计数器。默认是-1,字面意思是第-1个epoch已完成,也就是当前epoch从0算起,从头开始训练。
# 如果是加载checkpoint继续训练,那么这里要传入对应的已迭代次数。
# verbose:是否在更新学习率时在控制台输出提醒。
'''
创建学习率调度器:
使用 LambdaLR 创建一个学习率调度器 scheduler,将定义好的 lf 函数作为参数传入。
这个调度器会根据 lf 的定义动态调整给定优化器 optimizer 的学习率。
# plot_lr_scheduler(optimizer, scheduler, epochs) 是注释,暗示可以将调度函数与学习率的变化过程绘制出来,这将在日后分析学习率的效果时非常有用。
'''
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs)
'''
该段代码设置了一个学习率调度器,用于在模型训练过程中动态调整学习率。根据用户的选项,调度器可以选择使用余弦函数或线性函数的方式来调整学习率。
学习率是影响深度学习模型收敛速度和效果的重要超参数,因此这种动态调整机制有助于提升模型的训练效果和稳定性。
在训练过程中变更学习率可能会让训练效果更好,YOLOv5提供了两种学习率变化的策略:
一种是linear_lr(线性学习率),是通过线性插值的方式调整学习率
另一种则是One Cycle(余弦退火学习率),即周期性学习率调整中,周期被设置为1。
在一周期策略中,最大学习率被设置为 LR Range test 中可以找到的最高值,最小学习率比最大学习率小几个数量级。这里默认one_cycle。
'''
# EMA
# class ModelEMA: -> 更新后的指数移动平均线 (EMA)
# 在深度学习中用于创建模型的指数移动平均(Exponential Moving Average,EMA)的副本。通常,指数移动平均是用来平滑模型的参数,以提高模型的泛化能力。
# 在训练过程中,通常会使用 EMA 模型来获得更稳定的预测结果,而不是直接使用训练过程中的模型参数。这样可以减少模型在训练数据上的过拟合,并提高模型的泛化能力。
'''设置EMA 这段代码的主要功能是初始化一个指数移动平均(Exponential Moving Average, EMA)模型,用于在训练过程中对模型权重的平滑处理。'''
'''这里的注释指出该代码块与“指数移动平均”有关,EMA是一种常用的技巧,用于提高模型的鲁棒性和性能。'''
'''
条件表达式:
ModelEMA(model):这是一个调用ModelEMA类的构造函数,传入当前的模型model作为参数。ModelEMA类通常用于创建一个EMA模型,其权重会根据主模型的权重进行更新,以实现更平滑的学习过程。
if RANK in {-1, 0}:这是一个条件判断,用于判断当前训练环境的并行状态。
RANK是分布式训练中的一个参数,它表示当前进程的唯一标识符。在定义分布式训练时,RANK为-1通常表示单进程(单GPU)状态,而0表示主进程。在多进程环境中,其他进程的RANK值会大于0。
else None:如果RANK不是-1或0(即在非主进程的情况下),将ema设置为None,意味着在非主进程中不需要使用EMA模型。
'''
ema = ModelEMA(model) if RANK in {-1, 0} else None
'''
这段代码的主要功能是为了在训练过程中为主进程(单GPU或主进程)初始化一个EMA模型实例,以便利用EMA对训练过程中得到的权重进行平滑处理。
EMA模型可以增强模型的泛化能力,对于跟踪模型性能和在验证时选择更好的权重有帮助。而在其他非主进程中,因为无需进行指数移动平均处理,因此ema被设置为None。
EMA为指数加权平均或滑动平均。其将前面模型训练权重,偏差进行保存,
在本次训练过程中,假设为第n次,将第一次到第n-1次以指数权重进行加和,再加上本次的结果,且越远离第n次,指数系数越大,其所占的比重越小。
'''
'''设置断点续传参数 这段代码的功能是处理模型的恢复(resume)功能,具体来说,是在训练过程中从上一次训练的检查点继续训练。'''
# Resume 恢复
'''
初始化变量:这行代码初始化了两个变量:
best_fitness:用于存储当前最佳的模型表现(fitness)。通常表现为模型在验证集上的指标得分,比如精度(accuracy)或均值平均精度(mAP)。
start_epoch:用于记录训练开始的轮次(epoch),如果我们要恢复训练,这个值将指示从哪个轮次继续训练。
'''
best_fitness, start_epoch = 0.0, 0
'''检查预训练模型:这行代码检查当前是否使用预训练模型(pretrained 为真时),如果使用,则进入后续处理。'''
if pretrained:
# resume :恢复最近的训练
'''检查是否恢复训练:如果resume为真,表示用户希望从上一次训练的检查点重新开始训练。'''
if resume:
# smart_resume() -> 从部分训练的检查点恢复训练 -> best_fitness, start_epoch, epochs
'''
调用恢复函数:
这一行代码调用一个名为smart_resume的函数,并传入以下参数:
ckpt:包含模型状态、优化器状态、历史训练信息等的检查点文件。
optimizer:当前优化器的实例,可能需要恢复优化器的状态。
ema:表示模型的指数移动平均(Exponential Moving Average),用于帮助稳定训练。
weights:当前的权重文件路径。
epochs:总训练轮数。
resume:表示是否恢复训练的标志。
smart_resume函数返回新的best_fitness、start_epoch和epochs。这些值将根据检查点的状态进行更新。
'''
best_fitness, start_epoch, epochs = smart_resume(ckpt, optimizer, ema, weights, epochs, resume)
'''删除不再需要的变量:这行代码删除了ckpt和csd这两个变量,以释放内存。这些变量在恢复训练之后已经不再需要。'''
del ckpt, csd
'''设置DP模式'''
# DP mode DP 模式
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
# 警告⚠️不推荐使用 DP,使用 torch.distributed.run 可获得最佳 DDP 多 GPU 效果。
# 请参阅 https://github.com/ultralytics/yolov5/issues/475 上的多 GPU 教程以开始使用。
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.')
# 在深度学习的实践中,我们经常会遇到模型训练需要很长时间的问题,尤其是在处理大型数据集或复杂的神经网络时。
# 为了解决这个问题,我们可以利用多个GPU并行计算来加速训练过程。torch.nn.DataParallel() 是PyTorch提供的一个方便的工具,它可以让我们在多个GPU上并行运行模型的前向传播和反向传播。
# 简单来说,torch.nn.DataParallel() 将数据分割成多个部分,然后在不同的GPU上并行处理这些数据部分。
# 每个GPU都运行一个模型的副本,并处理一部分输入数据。最后,所有GPU上的结果将被收集并合并,以产生与单个GPU上运行模型相同的输出。
'''如果以上条件成立,此行代码会使用torch.nn.DataParallel来包装现有的模型,从而启用数据并行功能。这使得输入数据能够在多个GPU之间分配,利用多个GPU加速模型训练。'''
model = torch.nn.DataParallel(model)
'''设置同步批量归一化(SyncBatchNorm) 这段代码的主要功能是检查用户是否选择使用同步批量归一化(SyncBatchNorm),并在满足条件时将模型中的批量归一化层转换为同步批量归一化层,适用于分布式训练。'''
# SyncBatchNorm 同步批量规范
if opt.sync_bn and cuda and RANK != -1:
# torch.nn.SyncBatchNorm.convert_sync_batchnorm会搜索model里面的每一个module,如果发现这个module是、或者继承了torch.nn.modules.batchnorm._BatchNorm类,就把它替换成SyncBN。
# 也就是说,如果你的Normalization层是自己定义的特殊类,没有继承过_BatchNorm类,那么convert_sync_batchnorm是不支持的,需要你自己实现一个新的SyncBN!
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
LOGGER.info('Using SyncBatchNorm()') # 使用 SyncBatchNorm()
'''
初始化网络模型小结:
(1)载入模型:载入模型(预训练/不预训练) + 检查数据集 + 设置数据集路径参数(train_path、test_path) + 设置冻结层
(2)优化器:参数设置(nbs、accumulate、hyp[‘weight_decay’]) + 分组优化(pg0、pg1、pg2) + 选择优化器 + 为三个优化器选择优化方式 + 删除变量
(3)学习率:线性学习率 + one cycle学习率 + 实例化 scheduler + 画出学习率变化曲线
(4)训练前最后准备:EMA +断点续训+ 迭代次数的加载 + DP +SyncBatchNorm)
'''
'''初始化数据集 该段代码的主要功能是准备训练和验证数据加载器,并进行一些必要的处理。'''
# Trainloader
# create_dataloader() -> 创建数据加载器
'''
create_dataloader()函数用于创建一个训练数据加载器和数据集对象,到两个对象。
一个为train_loader,另一个为dataset。train_loader为训练数据加载器,可以通过for循环遍历出每个batch的训练数据。
dataset为数据集对象,包括所有训练图片的路径,所有标签,每张图片的大小,图片的配置,超参数等等。
参数说明:
train_path:训练数据集的路径。
imgsz:训练时图像的大小。
batch_size // WORLD_SIZE:每个GPU的批大小,支持多GPU训练。
gs:网格大小(最大步幅)。
single_cls:是否训练为单类(布尔值)。
hyp:超参数。
augment:是否进行数据增强。
cache:是否缓存图像,取决于给定的选项。
rect:是否进行矩形训练。
rank:当前进程的排名,用于分布式训练。
workers:数据加载的工作线程数量。
image_weights:使用加权图像选择进行训练。
quad:四连数据加载器模式。
prefix:用于显示的前缀字符串。
shuffle:是否随机打乱数据。
'''
train_loader, dataset = create_dataloader(train_path,
imgsz,
batch_size // WORLD_SIZE,
gs,
single_cls,
hyp=hyp,
augment=True,
cache=None if opt.cache == 'val' else opt.cache,
rect=opt.rect,
rank=LOCAL_RANK,
workers=workers,
image_weights=opt.image_weights,
quad=opt.quad,
prefix=colorstr('train: '),
shuffle=True)
'''合并标签:将数据集中所有图片的标签整合成一个数组,方便后续处理。'''
labels = np.concatenate(dataset.labels, 0)
'''
获取最大标签类:
mlc是标签中最大的类别索引。
assert语句检查最高标签类是否小于训练任务中的类别数量(nc),确保没有超出范围。
'''
mlc = int(labels[:, 0].max()) # max label class 最大标签类别
# 标签类 {mlc} 超出了 {data} 中的 nc={nc}。可能的类标签为 0-{nc - 1}
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' # nc 类别数量
# Process 0 进程 0
'''
处理验证数据加载器:
仅在主进程(RANK为-1或0)创建验证数据加载器。
create_dataloader()调用类似于训练数据加载器,但参数有所不同,以适应验证集的要求。
'''
if RANK in {-1, 0}:
# create_dataloader() -> 创建数据加载器
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 * 2,
pad=0.5,
prefix=colorstr('val: '))[0]
# resume恢复最近的训练
'''
模型锚框检查与精度预处理:
如果不是继续训练,则进行锚框检查(check_anchors()),以自动调整锚框的大小。
将模型转为半精度(half()),以提高性能并节省内存。
这段代码主要是计算默认锚点anchor与数据集标签框的长宽比值
check_anchors计算默认锚点anchor与数据集标签框的长宽比值,标签的长h宽w与anchor的长h_a宽w_a的比值, 即h/h_a, w/w_a都要在(1/hyp[‘anchor_t’], hyp[‘anchor_t’])是可以接受的,
如果标签框满足上面条件的数量小于总数的99%,则根据k-mean算法聚类新的锚点anchor。
'''
if not resume:
if not opt.noautoanchor:
# check_anchors() -> 检查锚点是否适合数据,如有必要重新计算
check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor 运行 AutoAnchor
model.half().float() # pre-reduce anchor precision 预降低锚框精度
# callbacks.run() -> 循环遍历已注册的操作并在主线程上触发所有回调
'''调用训练回调:调用回调函数,运行训练前例程结束的相关事件。'''
callbacks.run('on_pretrain_routine_end', labels, names)
'''
这段代码的核心功能是设置和预处理YOLOv5模型的训练和验证数据加载器。它创建数据集和加载器、整合标签并确保标签数量的合理性。
此外,还检查锚框的质量,以优化检测模型的性能,并在最后执行必要的回调。整个过程为后续模型的训练和验证做准备,确保数据的有效性和模型的性能。
'''
# DDP mode DDP 模式
'''分布式训练 这段代码的作用是针对分布式数据并行(Distributed Data Parallel, DDP)训练模式进行设置。'''
'''
条件判断 if cuda and RANK != -1:
cuda: 这是一个布尔值,表示当前是否使用GPU进行计算。如果为True,则说明程序在CUDA设备(GPU)上运行。
RANK: 这是一个整数,表示当前进程在分布式训练中的身份。通常,RANK为-1表示单个GPU训练,而非负值表示在多GPU环境下的进程编号。这里的判断是确保在使用多GPU的分布式模式下,代码将继续执行。
model = smart_DDP(model)
smart_DDP(model): 这个函数的作用是将模型包装成一个适合PyTorch的分布式数据并行(DDP)模型。
DDP可以有效地把模型的训练过程分发到多个GPU上,进而加速训练。函数smart_DDP根据当前的环境判断是否需要使用DDP,并处理相应的模型设置。
'''
if cuda and RANK != -1:
# smart_DDP() -> 使用 checks 创建模型 DDP
model = smart_DDP(model)
# Model attributes 模型属性
'''初始化训练需要的模型参数 这段代码的主要功能是对YOLOv5模型的某些属性进行初始化和调整,以适应特定的数据集和超参数设置。'''
# de_parallel() -> 取消并行化模型:如果模型类型为 DP 或 DDP,则返回单 GPU 模型
'''
这一行首先调用 de_parallel(model),它将模型从数据并行模式中转换为单个模型对象。
接着,.model[-1]表示获取模型的最后一层(通常是检测层),通过.nl获取该层的检测层数(number of detection layers,nl)。
这里通过nl来确定模型中需要调整的层数。
'''
nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps) 检测层的数量(缩放 hyps)
'''该行代码将超参数hyp中的box(边界框损失权重)按照检测层数nl进行缩放。原始的box损失权重值乘以3/nl,其目的是将损失权重调整到模型中的检测层数,以便适应不同的结构。'''
hyp['box'] *= 3 / nl # scale to layers 按层扩展
'''此处调整的是类别损失的权重。nc表示数据集中类别的数量,80是COCO数据集中类别的默认数量,这里用nc/80来进行缩放,再乘以3/nl,目的是为了平衡不同类别的损失在训练过程中的影响。'''
hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers 按类别和层进行扩展
'''
这一行代码则是调整目标损失(object loss)的权重。通过imgsz / 640来考虑输入图像的大小变化,640是默认的输入图像大小。
接着对结果进行平方计算,是因为损失与像素数量成比例,因此需要平方比例,同时也进行层数的缩放。
'''
hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers 缩放至图像大小和层
'''这里将标签平滑的超参数label smoothing设置为命令行参数中指定的值。这有助于避免过拟合,让模型对标签的某些变动更具鲁棒性。'''
hyp['label_smoothing'] = opt.label_smoothing
'''将模型的类别数(nc)设置为数据集中的类别数,以便模型能够正确地配置输出和计算损失。'''
model.nc = nc # attach number of classes to model 将类别数附加到模型
'''将超参数hyp附加到模型上,以便在训练过程中使用这些参数。'''
model.hyp = hyp # attach hyperparameters to model 将超参数附加到模型
# labels_to_class_weights() -> 训练标签中获取类别权重(逆频率) -> return torch.from_numpy(weights).float()
'''计算每个类别的权重,并将其存储在模型中,以提高不平衡类别的训练效果。labels_to_class_weights是一个函数,它生成基于数据集标签的类别权重。使用.to(device)将其移动到指定的设备(如GPU)。'''
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights 附加类别权重
'''将类别的名称(names)附加到模型中。这使得在模型推理和结果可视化时更容易理解哪些类别对应于输出。'''
model.names = names
'''
这段代码的主要功能是在训练YOLOv5模型之前,根据指定的超参数和数据集特性来初始化模型的各个属性。包括:
确定和调整检测层的数量。
根据类别的数量和输入图像的尺寸调整损失函数的权重。
设置标签平滑、类别数、超参数以及类别权重。
通过这样的初始化,模型能够适当地应对即将开始的训练任务,提高模型的学习效率以及最终的检测性能。
这段代码主要是根据自己数据集的类别数设置分类损失的系数,位置损失的系数。设置类别数,超参数等操作
其中,
box: 预测框的损失
cls: 分类的损失
obj: 置信度损失
label_smoothing : 标签平滑
'''
'''训练 这段代码是用于训练YOLOv5模型的核心部分。'''
'''
该代码段实现了YOLOv5模型的训练过程,包括数据加载、模型前向传递、损失计算、反向传播、优化步骤以及模型保存等。
它支持多种功能,如学习率调度、图像权重更新、早期停止等,以适应不同训练需求。
'''
# Start training 开始训练
'''训练开始'''
'''获取当前的时间戳,用于计算模型训练的总时间。'''
t0 = time.time()
'''train_loader是训练数据的加载器,len(train_loader)返回训练集中批次的总数量(即要处理的批次数)。nb代表总的批次数量。'''
nb = len(train_loader) # number of batches 批量数量
'''hyp['warmup_epochs']是一个超参数,表示预热阶段的训练周期数。
通过将预热周期数与批次数相乘来计算预热迭代的总数(nw),确保预热迭代数至少为100。
'''
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations) 预热迭代次数,最大值(3 个世代,100 次迭代)
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
'''初始化一个变量last_opt_step,用于跟踪上一次优化步骤的索引,初始设置为-1意味着还没有进行优化步骤。'''
last_opt_step = -1
''''创建一个大小为nc(类数)的零数组,用于存储每个类别的平均精度(mAP)。'''
maps = np.zeros(nc) # mAP per class 每类 mAP
'''初始化一个元组results,用于存储训练期间的各类指标:准确率(P),召回率(R),mAP@0.5,mAP@0.5-0.95,验证损失(框、目标、类别)等。'''
results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) ※※※※
'''对学习率调度器scheduler进行初始化,设置最后一个执行的epoch为start_epoch - 1,以保证调度器从正确的epoch开始更新学习率。'''
scheduler.last_epoch = start_epoch - 1 # do not move
# 当使用了混合精度训练,存在无法收敛的情况,原因是激活梯度的值太小了,造成了溢出。可以通过使用 torch.cuda.amp.GradScaler,放大loss的值 来防止梯度的下溢(underflow)。
# torch.cuda.amp.GradScaler(init_scale=65536.0, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True)
# init_scale :scale factor的初始值。
# growth_factor :每个scale factor 的增长系数。
# backoff_factor :scale factor 下降系数。
# growth_interval :每隔多个interval增长scale factor。
# enabled :是否做scale。
'''创建一个GradScaler实例,用于使用混合精度训练(如果启用),以防止梯度下溢,提高训练效率。'''
scaler = torch.cuda.amp.GradScaler(enabled=amp)
# class EarlyStopping: -> YOLOv5 简单早期停止器
'''初始化一个EarlyStopping实例stopper,用于监控训练过程中的早停条件,opt.patience表示在多少个epoch内没有改善时停止训练。stop用于指示是否要停止训练,初始为False。'''
stopper, stop = EarlyStopping(patience=opt.patience), False
# class ComputeLoss: -> 计算损失
'''初始化损失计算类ComputeLoss,用于计算模型在训练过程中的损失。'''
compute_loss = ComputeLoss(model) # init loss class 初始损失类别
# callbacks.run() -> 循环遍历已注册的操作并在主线程上触发所有回调
'''调用回调函数,在训练开始时执行任何注册的函数,以便执行相应的操作。'''
callbacks.run('on_train_start')
# 图像尺寸 {imgsz} train, {imgsz} val
# 使用 {train_loader.num_workers * WORLD_SIZE} 个数据加载器
# 将结果记录到 {colorstr('bold', save_dir)}
# 开始训练 {epochs} 个时期...
'''记录训练的相关信息,包括图像尺寸、数据加载器的工作线程数量以及将结果记录到的位置,并声明训练即将开始,总共要进行的epochs数量。'''
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n'
f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n'
f"Logging results to {colorstr('bold', save_dir)}\n"
f'Starting training for {epochs} epochs...')
# epochs总训练次数
'''训练循环'''
'''
训练循环框架
for epoch in range(start_epoch, epochs): # 遍历每个epoch
# end epoch
'''
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
# callbacks.run() -> 循环遍历已注册的操作并在主线程上触发所有回调
'''设置训练模式 '''
'''
这行代码调用了一个回调函数,传入的参数是字符串'on_train_epoch_start'。
回调机制在机器学习训练中常用于在训练过程的特定时刻执行用户自定义的操作,如记录日志、调整超参数或其他可由用户自定义的行为。
具体来说,在每个训练周期开始时,这个回调会被触发。这意味着这个回调可以用来执行在每个周期开始时需要的代码,比如记录当前的训练状态、更新可视化图表等。
这一行将模型切换到训练模式。在PyTorch中,模型有两种模式:训练模式(model.train())和评估模式(model.eval())。
切换到训练模式后,模型会启用某些特性,比如 dropout 和 batch normalization。这些特性在训练期间是必要的,以提高模型的泛化能力。
在训练模式下,模型的参数将会被更新(即进行反向传播和优化),而在评估模式下,模型的参数保持不变,只是用来计算验证集上的性能。
通过model.train()函数告诉模型已经进入了训练阶段。因为有些层或模型在训练阶段与预测阶段进行的操作是不一样的,所以要通过model.train()函数用来声明,接下来是训练。
'''
callbacks.run('on_train_epoch_start') # 在训练世代(epoch)开始时
''''# 设置模型为训练模式'''
model.train()
# Update image weights (optional, single-GPU only) 更新图像权重(可选,仅限单 GPU)
'''更新图片的权重'''
if opt.image_weights:
'''# 计算类权重'''
'''
model.class_weights.cpu().numpy():提取当前模型的类权重并将其移动到CPU,转换为NumPy数组。这通常是针对每一类物体的权重,用于在损失计算中对不同类的影响进行加权。
(1 - maps) ** 2:maps通常表示模型在各类数据上表现的一个指标(例如mAP)。1 - maps计算为每一类在当前模型下表现的劣势(分数越低,劣势越大)。对其平方是为了强调严重的表现劣势。
/ nc:将结果除以类的总数nc,以获取标准化的类权重。
'''
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights 类权重
# labels_to_image_weights() -> 根据 class_weights 和图像内容生成图像权重
'''# 计算图像权重'''
'''
这行代码调用了labels_to_image_weights函数,该函数计算每张图像的权重。其输入包括:
dataset.labels:数据集中每张图像的标签信息。
nc:类别的总数。
class_weights=cw:刚刚计算得到的类权重。这些权重会影响每张图像的整体重要性权重。
'''
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights 图像权重
'''# 随机选择索引'''
'''
range(dataset.n):生成一个范围,表示数据集中所有图像的索引。
weights=iw:使用计算得到的图像权重作为随机选择的权重。
k=dataset.n:表示要选择dataset.n个样本。根据权重选择图像使得那些重要性较高的图像被更频繁地采样。
'''
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx 随机加权 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,用于存储三个不同类型的平均损失(边框损失、目标损失和类别损失)。
张量被创建在指定的设备上(device),这可能是CPU或GPU。
'''
mloss = torch.zeros(3, device=device) # mean losses 平均损失
'''
设置训练数据加载器的epoch:
在这段代码中,如果RANK不等于-1,意味着训练是在分布式模式下进行的(比如多GPU训练),则调用train_loader中的sampler的set_epoch方法。
这是为了确保在每个epoch开始时,分布式数据加载器能够打乱数据,以避免每个epoch都使用相同的数据顺序,从而提高模型的泛化能力。
'''
if RANK != -1:
train_loader.sampler.set_epoch(epoch)
'''创建进度条:使用enumerate函数对train_loader进行包装,生成一个迭代器pbar,可以在后续的循环中得到当前批次的索引(i)和对应的数据(imgs, targets等)。'''
pbar = enumerate(train_loader)
'''记录训练信息:这行代码使用日志记录器LOGGER打印出一个格式化的日志信息,提供了一行标题,用于描述即将记录的训练统计信息,包括:当前epoch、GPU内存使用、边框损失、目标损失、类别损失、实例数和图像大小。'''
LOGGER.info(('\n' + '%11s' * 7) % ('Epoch', 'GPU_mem', 'box_loss', 'obj_loss', 'cls_loss', 'Instances', 'Size'))
'''进度条显示:这段代码检查RANK是否为-1或0,如果是,意味着这是主进程或单个设备模式下,使用tqdm将pbar转换为一个带有进度条的迭代器。total=nb指明进度条总共要显示的迭代次数,而bar_format=TQDM_BAR_FORMAT定义了进度条的格式。'''
if RANK in {-1, 0}:
# tqdm() 进度条 的常见参数有:
# iterable :要迭代的对象,可以是列表、元组、集合等可迭代对象。
# desc :进度条的描述文本,显示在进度条的左侧。
# total :迭代对象的总大小,用于计算进度百分比。如果不指定,则进度条将根据迭代对象的长度自动确定。
# leave :进度条完成后是否保留在输出中。值为True或False。
# ncols :进度条的宽度(以字符为单位),用于限制进度条的宽度。如果ncols的值为正整数N(N > 0),则进度条的宽度将被限制为N个字符。
# 默认情况下或者ncols的值为0、负数或None,则进度条的宽度将根据终端的宽度自动调整。
# bar_format :进度条的样式格式字符串。它可以包含特定的占位符,例如"{l_bar}{bar}{r_bar}"表示左边的文本、进度条本身和右边的文本。你可以在占位符中添加自定义的文本或符号来美化进度条。
# unit :进度条的单位名称,用于显示在进度百分比后面。例如,如果单位为"bytes",则进度条将显示为"10/100 bytes"。
# unit_divisor :进度条的单位除数,默认为1。可以用于将进度条的单位转换为更适合显示的单位格式。例如,如果单位为"bytes",但实际值是以KB为单位的,你可以将unit_divisor设置为1024,以便显示为"10/100 KB"。
# color :进度条的颜色,可以是ANSI颜色代码或预定义的颜色名称。ANSI颜色代码如,“\033[31m”表示红色,“\033[32m”表示绿色。
# tqdm库还提供了一些预定义的颜色名称,包括:black、red、green、yellow、blue、magenta、cyan、white。(如果设置颜色会报错,那可能是终端不支持)
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar 进度条
'''优化器梯度清零:在开始新一批的训练之前,调用 optimizer.zero_grad() 将模型的梯度清零。这是因为 PyTorch 默认会累积梯度,清零操作确保每批的梯度计算是独立的。'''
optimizer.zero_grad() # optimizer.zero_grad() ,用于将模型的参数梯度初始化为0。
'''
这段代码的主要功能是为训练过程的损失记录和进度条显示做准备:
它首先初始化一个用于存储损失的张量。
如果在分布式训练中,确保训练数据的打乱。
然后,使用枚举遍历数据加载器,为后续训练提供迭代器。
记录初始化的训练信息(如损失类型和内存使用情况)以便后续调试。
最后,创建一个进度条以可视化训练过程的进度,帮助监控训练的实时状态。
这段代码在训练深度学习模型时,提供了重要的损失度量与可视化反馈,有助于监控模型性能和确保训练过程的顺利进行。
'''
'''批处理操作 这段代码片段是一个训练循环的核心部分,主要负责处理每一个训练批次(batch),计算损失,进行反向传播,以及优化模型。'''
'''遍历数据批次:这里使用pbar(迭代器)来遍历数据加载器返回的批次。每个批次包含图像(imgs)、目标标签(targets)、图像路径(paths)以及一个未使用的占位符(_)。'''
for i, (imgs, targets, paths, _) in pbar: # batch -------------------------------------------------------------
# callbacks.run() -> 循环遍历已注册的操作并在主线程上触发所有回调
'''回调函数(Callbacks):在每个批次开始时,调用自定义的回调函数,这可以用于监控和记录训练过程。'''
callbacks.run('on_train_batch_start') # 训练批量启动
'''计算当前批次的编号:ni 表示自训练开始以来的集成批次数,通过当前批次 i 和总批次数 nb 和当前的 epoch 计算得出。'''
ni = i + nb * epoch # number integrated batches (since train start) 集成批量数(自训练启动以来)
'''数据准备:将图像数据转移到指定的计算设备(如GPU)上,同时将其转换为 float32 类型,并将值缩放到[0, 1]之间。'''
imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0 uint8 到 float32,0-255 到 0.0-1.0
# Warmup 预热
'''热身(Warmup)阶段:这段代码的主要功能是在训练过程中实现学习率的预热(warm-up)机制和梯度累积。这是在训练神经网络时的一种策略,目的是有效地提高模型的训练稳定性和性能。'''
'''条件检查:这行代码用于检查当前的迭代次数 ni 是否小于等于预热周期的总迭代次数 nw。ni 表示自训练开始以来的集成批次数,而 nw 是预热的迭代次数。'''
if ni <= nw:
''''定义插值区间:此行代码定义了一个变量 xi,它是一个列表,用于在插值时指定横坐标范围。这表示在前 nw 次迭代内进行插值。'''
xi = [0, nw] # x interp x 插值
# compute_loss.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou)
'''计算梯度累积的步数:使用 np.interp 计算当前的累积步数。nbs 是名义批量大小,batch_size 是当前的批量大小。此行的目的是确定当前迭代应该累积的步数,确保它在 1 及以上。如果当前迭代次数 ni 在预热范围内,累积步数会逐渐增加;否则,它将保持为 1。'''
accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round())
'''
更新优化器的学习率和动量:
这里有一个循环迭代 optimizer 中的每个参数组 x。
对于第一个参数组(通常是偏置优化器),使用 np.interp 更新学习率 lr,使其在预热期间从 warmup_bias_lr 逐渐过渡到 initial_lr * lf(epoch)。
其他参数组的学习率从 0 逐渐增大到其初始值。
'''
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 == 0 else 0.0, x['initial_lr'] * lf(epoch)])
if 'momentum' in x:
'''更新动量(如果适用):如果当前参数组 x 包含动量 (momentum),代码将根据迭代次数 ni 使用 np.interp 更新动量,从 warmup_momentum 过渡到指定的动量值。'''
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])
'''
这段代码的主要功能是实现学习率的预热和动量的平滑更新,以提高训练的稳定性。
通过逐渐增加学习率和可能的动量,可以避免在训练初期大幅度更新导致的梯度爆炸,从而使模型更平稳地收敛。
这是对模型训练过程中的一个重要优化步骤,常用于深度学习模型的训练中。
'''
# Multi-scale 多尺度
'''
多尺度训练:如果启用多尺度选项,则随机选择一个大小,以此对图像进行缩放,增加模型的泛化能力。
这段代码的目的是实现多尺度训练(Multi-scale training),允许模型在不同大小的输入图像上进行训练,从而增强模型的鲁棒性。
'''
if opt.multi_scale:
''''
随机选择新的图像大小:这里使用 random.randrange 从 imgsz(原始图像大小)的50%到150%之间随机选择一个新的图像大小。
gs 是网格大小(即模型的最大步幅),这行代码确保选择的尺寸是 gs 的倍数。也就是说,图像大小的变化不会影响到模型输入的格式。
'''
sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs # size 尺寸
'''计算缩放因子:计算当前图像的缩放因子 sf,该因子基于新选择的尺寸 sz 除以当前输入图像的最大宽度或高度(注意,当前图像的形状为 (batch_size, channels, height, width))。'''
sf = sz / max(imgs.shape[2:]) # scale factor 比例因子
'''
调整图像大小:
这部分首先检查缩放因子 sf 是否不等于1(即如果新的图像大小与当前大小相同就不做处理)。如果需要调整:
(1) 通过列表推导式计算新的图像大小 ns,确保这些新的大小也都是 gs 的倍数。
(2) 然后使用 torch.nn.functional.interpolate 函数基于新的大小 ns 对输入图像进行插值调整,使用双线性插值法(mode='bilinear'),并且不对齐角点(align_corners=False)。
'''
if sf != 1:
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple) 新形状(拉伸至 gs-multiple)
# torch.nn.functional.interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None, antialias=False)
# torch.nn.functional.interpolate 是 PyTorch 中用于对张量进行上采样或下采样的函数。它支持多种插值方法,例如双线性插值、最近邻插值等,广泛用于图像处理、特征图缩放等场景。
# 参数:
# input : 需要进行插值的输入张量,通常是一个 3D(N, C, L)或 4D(N, C, H, W)或 5D(N, C, D, H, W)张量,
# 其中 N 是批次大小,C 是通道数,L、H、W、D 分别是长度、高度、宽度和深度。
# size : 输出张量的目标大小。如果指定了此参数,scale_factor 将被忽略。
# scale_factor : 缩放因子,可以是一个数字或包含三个数字的元组,对应于各个维度的缩放因子。
# mode : 插值的算法类型。常见的值包括 'nearest', 'linear', 'bilinear', 'bicubic', 'trilinear', 'area'。
# align_corners : 用于双线性和三线性插值。如果为 True,输入和输出张量的角点将对齐。默认为 False。
# recompute_scale_factor : 重新计算 scale_factor 的布尔值或 None。如果设置为 True,则基于计算的输出大小重新计算 scale_factor。
# antialias : 如果为 True 并且 mode 是 ‘bilinear’,‘bicubic’ 或 ‘trilinear’ 时,会应用抗锯齿滤波。默认为 False。
# 返回值:
# 返回插值后的张量,大小和形状由 size 或 scale_factor 确定。
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False)
'''
这段代码的主要功能是实现多尺度训练,通过随机选择不同的图像大小(在原始尺寸的50%到150%之间),并动态调整输入图像的大小,
使得训练过程中模型能接触到不同大小的输入数据,从而提高其在真实场景下的鲁棒性和泛化能力。
这样的做法在目标检测和图像分割等任务中常常被用来避免模型对特定输入尺寸的依赖。
'''
# Forward 前向传播
# 当使用了混合精度训练,存在无法收敛的情况,原因是激活梯度的值太小了,造成了溢出。可以通过使用 torch.cuda.amp.GradScaler,放大loss的值 来防止梯度的下溢(underflow)。
# torch.cuda.amp.GradScaler(init_scale=65536.0, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000, enabled=True)
# init_scale :scale factor的初始值。
# growth_factor :每个scale factor 的增长系数。
# backoff_factor :scale factor 下降系数。
# growth_interval :每隔多个interval增长scale factor。
# enabled :是否做scale。
'''前向传播:使用自动混合精度(AMP)进行前向传播,计算模型的预测输出,然后与真实标签计算损失。'''
'''
上下文管理:'
使用 torch.cuda.amp.autocast 进行自动混合精度(Automatic Mixed Precision, AMP)管理。
混合精度训练可以提高训练速度和减少显存使用,它会根据不同的运算自动选择使用浮点16(FP16)或浮点32(FP32)来计算。在这里,参数 amp 是一个布尔值,指示是否启用自动混合精度。
'''
with torch.cuda.amp.autocast(amp):
'''模型前向传播:这行代码将输入图像 imgs 传递给模型 model,进行前向传播,得到预测结果 pred。模型会根据其结构和训练的参数对输入数据进行处理,返回预测值。'''
pred = model(imgs) # forward
# compute_loss = ComputeLoss(model)
# def __call__(self, p, targets): -> return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()
'''
损失计算:使用 compute_loss 函数计算损失。将预测值 pred 和目标值 targets(目标值也被转移到设备上,例如 GPU)传入此函数,得到损失值 loss 和损失项
loss_items。损失值是衡量模型预测与实际目标的差距,通常用于优化模型。
'''
loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size 损失按 batch_size 缩放
'''
分布式训练中损失的调整:这段代码用来处理分布式数据并行(Distributed Data Parallel, DDP)模式下的损失计算。
当 RANK 不等于 -1 时,表示当前进程处于多进程状态。在这种情况下,损失会乘以 WORLD_SIZE(表示参与训练的设备总数),以确保在多设备情况下,优化计算出的梯度是正确的。
'''
if RANK != -1:
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode DDP 模式下设备间梯度平均
'''
损失调整:
这里检查另一个布尔选项 opt.quad,如果为真,就将损失值 loss 乘以4。当启用四元组数据加载(Quad DataLoader)时,可能需要调整损失值以适应加载逻辑。
'''
if opt.quad:
loss *= 4.
'''
这段代码的主要功能是在神经网络的训练过程中执行前向传播并计算损失。在实现自动混合精度的情况下,它通过模型预测获取输出,对预测值与实际目标之间的差距进行损失计算,
并根据训练设备的数量适当地调整损失。这对于保证在分布式训练和不同训练策略下,损失计算和反向传播的有效性至关重要。
'''
# Backward 反向传播
'''
反向传播:
在损失的基础上进行反向传播,使用AMP的缩放器(scaler)来处理梯度。
这一行代码涉及到深度学习中的反向传播过程,特别是与混合精度训练相关的操作。
上下文理解:
这段代码通常出现在一个训练循环中。在深度学习中,训练模型的一个重要步骤是通过反向传播算法更新模型的权重。反向传播利用损失函数的梯度来调整神经网络的参数。
loss 变量:
在这行代码之前,模型的输出会与实际的目标进行比较,计算出损失(loss)。这个损失值反映了模型在当前参数下的预测与实际的偏差。
scaler 对象:
scaler 是一个 torch.cuda.amp.GradScaler 对象,用于混合精度训练。混合精度训练的目的是提高模型训练效率,减少内存使用,同时保持模型性能。
GradScaler 通过动态调整损失的缩放因子来避免在较小的梯度更新中发生数值下溢,确保梯度在反向传播过程中不会丢失信息。
scaler.scale(loss):
scaler.scale(loss) 的作用是对损失进行缩放(scale),以适应混合精度训练。这意味着它将损失值乘以一个缩放因子。
这种方式可以提高计算的稳定性,尤其是在使用半精度浮点数(float16)进行训练时。
.backward() 方法:
backward() 方法用于计算损失函数相对于模型参数的梯度。它会通过链式法则自动计算各个参数(权重和偏置)的梯度,并存储在相应的 parameter.grad 属性中,以便稍后更新。
这一行代码的主要功能是实现反向传播过程中的梯度计算,同时考虑混合精度训练的影响。
通过使用 GradScaler 对损失进行缩放,它确保了在使用半精度浮点数进行计算时,梯度不会出现数值下溢的问题。
整体来说,这行代码是深度学习模型训练中反向传播步骤的重要组成部分,尤其是在使用混合精度训练以提升训练效率的场景下。
'''
scaler.scale(loss).backward()
# Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
# 优化 - https://pytorch.org/docs/master/notes/amp_examples.html
'''优化步骤:当达到预定的累积步数时,更新优化器的参数,并使用梯度裁剪以避免梯度爆炸。这段代码的主要功能是执行模型的梯度优化过程,具体用于训练过程中更新模型的参数。'''
'''这一行判断当前的训练步数 ni 和上次优化步数 last_opt_step 之间的差值是否大于或等于一个预定的累积步数 accumulate。这意味着在每次更新模型参数之前,需要累积一定数量的梯度。'''
if ni - last_opt_step >= accumulate:
'''这行代码用于将优化器的梯度“反缩放”回原始大小。这与混合精度训练(Mixed Precision Training)有关,在它中,损失是以较高精度(如 float32)计算的,而优化器的梯度以较低精度(如 float16)进行更新。因此,在更新梯度之前,必须将其反缩放到原始规模。'''
scaler.unscale_(optimizer) # unscale gradients 不缩放梯度
# 梯度裁剪是为了防止梯度爆炸。
# torch.nn.utils.clip_grad_norm_(parameters, max_norm, norm_type) ,这个梯度裁剪函数一般来说只需要调整max_norm 和norm_type这两个参数。
# parameters 参数是需要进行梯度裁剪的参数列表。通常是模型的参数列表,即model.parameters()
# max_norm 参数可以理解为梯度(默认是L2 范数)范数的最大阈值
# norm_type 参数可以理解为指定范数的类型,比如norm_type=1 表示使用L1 范数,norm_type=2 表示使用L2 范数。
# torch.nn.utils.clip_grad_norm_() 直接修改原Tensor
'''这一行用于梯度裁剪(Gradient Clipping),限制模型参数的梯度范数不超过最大值 max_norm(这里为10.0)。这可以防止梯度爆炸,确保训练过程更加稳定。'''
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) # clip gradients 梯度裁剪
'''这行代码告诉优化器根据当前的梯度来更新模型的参数。此操作基于混合精度训练的设置,能够有效地利用GPU的计算能力。'''
scaler.step(optimizer) # optimizer.step
'''这一行使得梯度缩放器更新其内部状态,以便为下一个优化步骤准备。这是与混合精度训练相关的一部分,以确保后续的梯度计算是准确的。'''
scaler.update()
'''这行代码在更新参数后,将所有模型参数的梯度清零,准备下一次反向传播。这是为了避免梯度累积(除非是故意设置的累积)。'''
optimizer.zero_grad()
'''这一行检查是否使用了指数移动平均(Exponential Moving Average,EMA),这是一种平滑模型训练过程的方法。'''
if ema:
'''如果使用了EMA,这行代码更新EMA模型的参数。EMA模型通常在训练结束时用于推断,以提高模型的稳定性和准确性。'''
ema.update(model)
'''最后,这一行更新 last_opt_step 变量为当前训练步数 ni,以便在下次优化步骤时进行比较。'''
last_opt_step = ni
'''
这段代码实现了在训练过程中对模型参数的优化,包括梯度反缩放、裁剪和参数更新,确保训练稳定、效率高。
它主要用于混合精度训练,利用 GPU 的优势来加速计算,并通过 EMA 提高模型性能。
'''
# Log
'''记录和日志'''
if RANK in {-1, 0}:
'''
这一行代码的目的是检查当前正在运行的进程的RANK值。RANK通常用于多GPU训练,其中-1表示单核训练(即没有使用分布式数据并行),而0表示主进程。
在分布式训练中,只有主进程或单核进程才会进行日志记录和输出,因此仅当RANK是-1或0时,这段代码才会执行。
'''
'''
更新平均损失:
在这行代码中,mloss代表当前批次的平均损失。通过将当前批次的损失(loss_items)加权平均到mloss中,实现对所有已处理批次损失的更新。其中i表示当前是第几个批次,因此使用i + 1来防止除以零。
'''
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses 更新平均损失
'''计算和记录当前GPU的显存使用情况:这段代码计算当前使用的显存量,如果CUDA可用,则使用torch.cuda.memory_reserved()来获取显存量并转换为GB(十亿字节);否则值为0。输出的格式保留了三位有效数字。'''
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB)
# 使用 pbar.set_description() 指定进度条左侧(前缀)显示的信息
'''
更新进度条描述:这一行代码调用定义好的回调函数,通知训练已完成一个批次。
传递的参数包括当前模型model、当前集成批次的索引ni、当前输入的图像imgs、目标targets、图像路径paths以及损失列表list(mloss)。
'''
pbar.set_description(('%11s' * 2 + '%11.4g' * 5) %
(f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1]))
callbacks.run('on_train_batch_end', model, ni, imgs, targets, paths, list(mloss))
'''检查是否需要停止训练:最后,如果在回调函数中设置了stop_training为True,程序会立即返回,结束训练。'''
if callbacks.stop_training:
return
'''
这段代码主要负责在每个训练批次结束时进行日志记录与更新。具体功能包括:
更新和计算当前批次的平均损失。
记录显存使用情况,以便监控资源分配。
更新训练进度条显示信息,包括当前epoch、显存、损失、目标数量和图像大小等。
调用回调函数,允许用户自定义的操作(如记录日志、保存模型等)。
检查是否应提前停止训练。
整体上,这部分代码的作用是增强训练过程的可视化和可控制性,为开发者提供实时的训练反馈和监控。
'''
# end batch ------------------------------------------------------------------------------------------------
'''更新学习率调度器 这段代码的功能是更新学习率调度器(scheduler)以调整训练过程中模型的学习率。'''
# Scheduler 调度器
'''
获取学习率:
这一行代码遍历优化器(optimizer)的参数组(param_groups)中的每一个参数组,并从中提取当前的学习率('lr')。
optimizer.param_groups 是一个包含所有参数组的列表,每个参数组都是一个字典,包含了学习率、权重衰减等信息。
通过列表推导式,我们将所有参数组的学习率存储到列表 lr 中。这个步骤通常是为了记录当前的学习率以便于后续的日志记录或调试。
'''
lr = [x['lr'] for x in optimizer.param_groups] # for loggers
'''
更新调度器:
这一行调用了学习率调度器的 step() 方法,目的是更新学习率。根据设定的调度策略(如预热、余弦退火等),调度器会调整当前使用的学习率。
这个方法会根据当前的epoch或batch更新学习率,为后续的训练步骤提供新的学习率。
'''
scheduler.step()
'''
这段代码的主要功能是实现学习率的动态调整。在深度学习训练过程中,合理地更新学习率可以帮助模型更好地收敛,提高训练效率。
通过调用 scheduler.step(),模型在每个训练周期或步骤后可以根据预设的调度策略自动调整学习率,从而适应训练过程中的不同阶段。
'''
'''
每个epoch训练结束时操作
这段代码主要是处理模型训练周期结束时的操作,包括:
进行模型的验证和计算mAP;
更新最佳性能指标和执行条件检查以实现早停;
记录和保存训练日志;
保存模型状态。
这些步骤对于监控训练过程和模型性能至关重要,有助于确保在训练结束时获得最佳的模型。并借助EMA和回调机制,提升整体训练的可管理性和可扩展性。
'''
'''这一行判断当前的训练进程是否是主进程(RANK为-1或0),只有主进程会执行后续代码。这通常用于多GPU训练,确保只在一个进程中执行特定操作,避免重复工作。'''
'''这段代码主要用于训练模型的最后一步,负责更新最佳的平均精度(mean Average Precision, mAP),并检查是否满足提前终止训练的条件。'''
if RANK in {-1, 0}:
# mAP
'''回调函数执行:在每个训练周期结束时,调用回调函数,传入当前的周期数。这通常用于记录和跟踪训练过程。'''
callbacks.run('on_train_epoch_end', epoch=epoch)
'''
更新EMA(指数移动平均)属性:这行代码更新模型的EMA属性,
包括模型的配置(yaml),类数(nc),超参数(hyp),类名(names),步幅(stride),和类权重(class_weights)。
EMA用于平滑训练过程中的模型权重,从而提高最后模型的性能。
'''
ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights'])
'''确定是否为最后一个周期:该行检查当前是否已经是最后一个训练周期,或者早期停止条件是否满足。'''
final_epoch = (epoch + 1 == epochs) or stopper.possible_stop
# nosave仅保存最终检查点
'''计算mAP:如果不进行验证(noval)或是最后一个周期,调用验证过程并计算平均精准率(mAP)。validate.run函数传入必要参数,包括数据字典、批处理大小、图像大小、模型、验证数据加载器等。'''
if not noval or final_epoch: # Calculate mAP 计算 mAP
# validate.run() -> return (*final_metric, *(loss.cpu() / len(dataloader)).tolist()), metrics.get_maps(nc), t
# (边界框mp,边界框召回率mr,边界框map50,边界框map,掩膜mp,掩膜召回率mr,掩膜map50,掩膜map,每个批量大小数量的幅图片的损失(边界框损失,目标损失,分类损失)),
# 每个类别边界框和掩膜的度量的map,
# 每幅图像的速度(预处理时间、推理时间、后处理时间)
# (box(p, r, map50, map), mask(p, r, map50, map), *loss(box, obj, cls))
# map(所有类别的mAP@0.5:0.95)
# times (preprocess, inference, postprocess)
'''
测试使用的是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, _ = validate.run(data_dict,
batch_size=batch_size // WORLD_SIZE * 2,
imgsz=imgsz,
half=amp,
model=ema.ema,
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
plots=False,
callbacks=callbacks,
compute_loss=compute_loss)
# Update best mAP 更新最佳 mAP
# fitness() -> 模型适应度作为指标的加权组合
'''计算当前的适应度值(fitness):这行代码的作用是计算模型在验证集上的性能评估指标,并将结果存储在变量 fi 中。'''
'''
fitness() 是一个函数,通常用于计算综合性能指标。在目标检测任务中,常用的性能指标包括精确度(Precision)、召回率(Recall)、平均精确度(mAP),其
中 mAP 可能会在不同的 IoU(Intersection over Union)阈值下进行计算,比如 mAP@0.5 和 mAP@0.5:.95。
results 是一个包含模型在验证集上评估结果的列表或其他结构,可能包括多个指标。
np.array(results) 将 results 转换成 NumPy 数组,以便后续的操作。
reshape(1, -1):该操作将 NumPy 数组 results 重新调整形状,使其变成一个行向量。
具体来说,reshape(1, -1) 表示创建一个只有一行的数组,而列的数量则由原数组的总元素数决定。这样做的目的是为了确保输入的维度符合 fitness 函数的要求。
最终,将计算得到的综合性能指标结果赋值给变量 fi。
'''
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] [P,R,mAP@.5,mAP@.5-.95] 的加权组合
# stopper = EarlyStopping(patience=opt.patience)
'''进行提前终止检查:这行代码的作用是执行一个早期停止检查,以决定是否停止训练过程。'''
'''
stopper 是一个实例,通常是 EarlyStopping 类的一个对象。这个类的功能主要是监控训练过程的性能指标,如验证集上的损失值或准确率,并根据设定的条件判断是否应该早期停止训练。
早期停止的意义在于防止模型过拟合。当模型在训练集上表现良好,但在验证集上的表现开始恶化时,通常会选择停止训练以保留最佳模型。
epoch=epoch
这是将当前的训练周期(epoch)传递给 stopper,通常是在循环中的当前周期计数(从 0 开始)。这个参数可以让 stopper 知道目前进行到哪个训练周期。
fitness=fi
fi 代表模型当前的“适应度”(fitness)值,这个值通常是根据模型在验证集上的表现计算得出的,可能涉及到精确率(Precision)、召回率(Recall)和 mAP(Mean Average Precision)等指标。
通过将适应度值作为参数传入,stopper 可以基于这个值来判断是否满足停止训练的条件。
这个赋值操作将 stopper 函数的返回值赋给变量 stop。这个布尔值将指示是否应该提前停止训练。通常,如果返回 True,则意味着应该停止训练;如果返回 False,则训练继续。
'''
stop = stopper(epoch=epoch, fitness=fi) # early stop check 提前停止检查
'''更新最佳适应度值:通过比较当前适应度 fi 与之前记录的最佳适应度 best_fitness,若当前适应度更高,则更新最佳适应度值。这使得模型可以随时保存最佳历史性能,便于后续模型评估与选择。'''
if fi > best_fitness:
best_fitness = fi
'''
记录训练日志:
将当前轮次的损失 mloss 和验证结果 results、学习率 lr 组合成一个列表 log_vals,便于记录和可视化。
callbacks.run('on_fit_epoch_end', ...) 用于在训练的某一轮结束后执行注册的操作,例如记录日志、更新可视化等等。这是一个设计模式,允许用户在不同训练阶段插入自定义操作。
'''
log_vals = list(mloss) + list(results) + lr
callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi)
'''
这段代码主要负责更新和监控训练过程中的模型性能。通过计算当前模型在验证集上的适应度,检查是否需要提前停止训练,并在每个训练轮次结束时记录相关日志信息。
其主要功能是确保模型在训练过程中能够自我评估和优化,从而达到最佳的性能。
'''
# Save model 保存模型
'''模型保存 这段代码主要用于保存训练过程中的模型以及相关的状态信息。'''
'''
条件检查:
osave:如果此标志为 True,则表示不保存模型。
final_epoch:检查当前是否是最后一个训练周期。
evolve:如果处于超参数进化的状态,通常不保存模型。
本行代码的意思是:如果 nosave 为 False,或者当前是最后一个周期且没有进行超参数进化,则继续执行保存模型的操作。
'''
if (not nosave) or (final_epoch and not evolve): # if save 如果保存
'''
构建检查点:
epoch:当前训练的周期数。
best_fitness:最佳适应度值,即模型在验证集上表现最好的结果。
model:深拷贝去并行化后的模型,并转换为半精度(half),以减少内存占用。
ema:用以保存指数移动平均(Exponential Moving Average)模型的状态。
updates:用于记录 EMA 的更新次数。
optimizer:保存优化器的状态字典,以便后续恢复训练。
opt:保存训练时的参数设置。
git:保存与 Git 相关的信息(如远程地址、分支、提交等)。
date:当前时间的 ISO 格式字符串,记录保存操作的时间。
'''
ckpt = {
'epoch': epoch,
'best_fitness': best_fitness,
# de_parallel() -> 取消并行化模型:如果模型类型为 DP 或 DDP,则返回单 GPU 模型
'model': deepcopy(de_parallel(model)).half(),
'ema': deepcopy(ema.ema).half(),
'updates': ema.updates,
'optimizer': optimizer.state_dict(),
'opt': vars(opt),
'git': GIT_INFO, # {remote, branch, commit} if a git repo
'date': datetime.now().isoformat()}
# Save last, best and delete 保存最后、最佳并删除
'''保存模型:使用 torch.save 方法将保存的检查点 ckpt 存储到 last 文件中,通常这个文件用于保存最新的模型状态。'''
torch.save(ckpt, last)
'''保存最佳模型:如果当前的适应度(fi)等于最佳适应度值,说明当前模型是最好的,那么将它存储到 best 文件中。'''
if best_fitness == fi:
torch.save(ckpt, best)
'''定期保存模型:如果设置了保存周期(save_period)并且当前周期是保存的周期,便将当前检查点保存为 epoch{epoch}.pt 文件。w 是用于保存模型的路径。'''
if opt.save_period > 0 and epoch % opt.save_period == 0:
torch.save(ckpt, w / f'epoch{epoch}.pt')
'''删除检查点:删除 ckpt 变量,以释放内存。'''
del ckpt
'''调用回调函数,传递有关模型保存的信息,这通常用于更新日志或其他后续处理。'''
callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi)
# EarlyStopping 提早停止
'''早期停止检查:这段代码的目的是在训练过程中实现“提前停止”(Early Stopping)功能,主要用于分布式数据并行(DDP)训练的场景。'''
'''判断 RANK 是否不等于 -1:这一行代码检查当前训练是否在分布式模式下运行。RANK变量是用于标识当前进程的编号,-1表示单进程模式。'''
if RANK != -1: # if DDP training
broadcast_list = [stop if RANK == 0 else None]
dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks 向所有级别广播‘停止’
if RANK != 0:
stop = broadcast_list[0]
if stop:
break # must break all DDP ranks 必须打破所有 DDP 等级
'''
这段代码实现了分布式训练中的提前停止机制。通过使用 PyTorch 的分布式通信功能(如 broadcast_object_list),它确保所有参与训练的进程都能依据主进程的停止条件一致地停止训练。
这样做的好处是在面对大规模训练时,可以有效避免不必要的资源浪费,尤其是在模型不再提升性能的情况下。
'''
# end epoch ----------------------------------------------------------------------------------------------------
'''训练结束 这段代码是一个深度学习训练过程中的结束部分,主要负责完成训练后的一些收尾工作。'''
# end training -----------------------------------------------------------------------------------------------------
'''检查 RANK:这行代码检查当前进程的 RANK 值。RANK 为 -1 表示单机单卡训练,0 表示主进程。在分布式训练中,通常只有主进程负责输出日志和保存模型。'''
if RANK in {-1, 0}:
# {epoch - start_epoch + 1} 个世代(epochs)在 {(time.time() - t0) / 3600:.3f} 小时内完成。
'''记录训练时间和完成的轮次:这行代码记录了训练完成的轮次数和所花费的总时间。t0 是开始训练时的时间戳,计算的时间以小时为单位表示。'''
LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.')
'''处理模型文件:这段代码遍历两个文件:最新模型(last.pt)和最佳模型(best.pt)。接下来的步骤会处理这些文件。'''
for f in last, best:
'''检查文件是否存在并处理:如果文件存在,则调用 strip_optimizer(f) 函数去除模型文件中的优化器状态,以减小文件大小。这在后续的验证和使用模型时,通常不需要优化器信息。'''
if f.exists():
# strip_optimizer() -> 从“f”中剥离优化器以完成训练,可选择保存为“s”
strip_optimizer(f) # strip optimizers
''''
验证最佳模型:
当处理的是最佳模型时,这段代码会进行验证。具体而言:
调用 LOGGER.info() 输出正在验证的信息。
使用 validate.run() 函数对模型进行验证,传入必要的参数,如数据集、批处理大小、模型及其文件路径、评估指标等。
iou_thres 根据是否使用 COCO 数据集设置不同的 IOU 阈值。
'''
if f is best:
LOGGER.info(f'\nValidating {f}...') # 正在验证 {f}...
# validate.run() -> return (*final_metric, *(loss.cpu() / len(dataloader)).tolist()), metrics.get_maps(nc), t
# (边界框mp,边界框召回率mr,边界框map50,边界框map,掩膜mp,掩膜召回率mr,掩膜map50,掩膜map,每个批量大小数量的幅图片的损失(边界框损失,目标损失,分类损失)),
# 每个类别边界框和掩膜的度量的map,
# 每幅图像的速度(预处理时间、推理时间、后处理时间)
# (box(p, r, map50, map), mask(p, r, map50, map), *loss(box, obj, cls))
# times (preprocess, inference, postprocess)
results, _, _ = validate.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 at iou 0.65
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
save_json=is_coco,
verbose=True,
plots=plots,
callbacks=callbacks,
compute_loss=compute_loss) # val best model with plots
'''运行回调:如果数据集是 COCO,调用相关回调函数来处理训练结束后的逻辑,比如记录训练指标。'''
if is_coco:
callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi)
'''结束训练的回调:最后,运行训练结束的回调,这里会传入最后的模型、最佳模型、当前轮数和验证结果。'''
callbacks.run('on_train_end', last, best, epoch, results)
'''清空缓存:清空 PyTorch 的 CUDA 缓存,以释放显存。这样可以优化后续计算的内存使用。'''
torch.cuda.empty_cache()
return results
3.def parse_opt(known=False):
'''parse_opt() -> 解析命令行参数,以便在运行训练程序时可以通过参数指定相关设置。'''
def parse_opt(known=False):
# argparse 是一个Python模块:命令行选项、参数和子命令解析器。通过命令行运行Python脚本时,可以通过ArgumentParser来高效地接受并解析命令行参数。
# 1. 创建一个ArgumentParser对象,这个对象会保存我们定义的所有命令行参数信息。
# parser = argparse.ArgumentParser(description='这是一个示例程序') 这里的description参数是可选的,它用于描述这个命令行程序的主要功能
# add_argument() 方法用于添加一个你希望程序接受的命令行参数。
# parser.add_argument('input', type=str, help='输入文件的路径')
# 参数类型:
# *位置参数:这些参数是必须的,使用时需按正确的顺序提供。*可选参数:通常以-或--开始,不需要按特定顺序。
# add_argument 的常见参数:
# help :描述参数作用的字符串。
# type :命令行参数应转换成的Python类型。
# default :如果命令行中未提供参数,则使用的默认值。
# required :可选参数是否可以省略(仅对可选参数有效)。
# choices :参数值限制为特定的选项。
# action :当参数在命令行中存在时采取的行动。
# 可以使用 choices 参数来限制用户只能选择特定的值。
# 例子:
# parser.add_argument('-v', '--verbose', action='store_true', help='显示详细输出')
# -v和--verbose是同一个可选参数的两种形式,它们的类型是布尔值,当用户在命令行中指定这个参数时,它的值为True,否则为False。
# action='store_true'表示当指定这个参数时,将其值设置为True。
# 2. 使用 parse_args() 方法来解析命令行参数。
# args = parser.parse_args()
# 3. 然后,我们就可以通过args.参数名的方式来访问这些参数的值了
parser = argparse.ArgumentParser()
'''--weights :初始权重文件的路径,如果这里设置为空的话,就是自己从头开始进行训练。'''
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path') # 初始权重路径
parser.add_argument('--cfg', type=str, default='', help='model.yaml path') # model.yaml 路径
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') # dataset.yaml 路径
'''--hyp : 超参数文件的路径,超参数里面包含了大量的参数信息。'''
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path') # 超参数路径
parser.add_argument('--epochs', type=int, default=100, help='total training epochs') # 总训练次数
'''--batch-size :每批次的输入数据量;default=-1将时自动调节batchsize大小。'''
'''
epoch、batchsize、iteration三者之间的联系
1、batchsize是批次大小,假如取batchsize=16,则表示每次训练时在训练集中取16个训练样本进行训练。
2、iteration是迭代次数,1个iteration就等于一次使用16(batchsize大小)个样本进行训练。
3、epoch:1个epoch就等于使用训练集中全部样本训练1次。
'''
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch') # 所有 GPU 的总批量大小,自动批处理为 -1
'''--imgsz :图像的大小(以像素为单位),输入默认 640 × 640。 图片越大,训练速度越慢。 图像在训练时resize同样花费时间的。'''
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)') # 训练,测试 图像大小(像素)
'''--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 :是否只保存最后一轮的pt文件;默认是保存 best.pt 和 last.pt 两个的。'''
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') # 仅保存最终检查点
'''--noval :只在最后一轮测试;正常情况下每个epoch都会计算mAP,但如果开启了这个参数,那么就只在最后一轮上进行测试,改进模型时不建议开启,因为会看不到曲线变化情况。'''
parser.add_argument('--noval', action='store_true', help='only validate final epoch') # 仅验证最后一个 epoch
'''--noautoanchor :是否禁用自动锚框;默认是开启的,自动锚框的好处是可以简化训练过程。'''
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor') # 禁用 AutoAnchor
parser.add_argument('--noplots', action='store_true', help='save no plot files') # 不保存任何情节文件
'''--evolve :遗传超参数进化,由于超参数进化会耗费大量的资源和时间,所以建议不要动这个参数。'''
'''
遗传算法
是利用种群搜索技术将种群作为一组问题解,通过对当前种群施加类似生物遗传环境因素的选择、交叉、变异等一系列的遗传操作来产生新一代的种群,
并逐步使种群优化到包含近似最优解的状态,遗传算法调优能够求出优化问题的全局最优解,优化结果与初始条件无关,算法独立于求解域,具有较强的鲁棒性,适合于求解复杂的优化问题,应用较为广泛。
'''
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations') # 进化 x 代的超参数
'''--bucket :谷歌云盘;通过这个参数可以下载谷歌云盘上的一些东西'''
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') # 谷歌云盘
'''--cache :是否提前缓存图片到内存,以加快训练速度,默认False;开启这个参数就会对图片进行缓存,从而更好的训练模型。'''
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk') # 图像--缓存内存/磁盘
'''--image-weights :是否启用加权图像策略,默认是不开启的;主要是为了解决样本不平衡问题;开启后会对于上一轮训练效果不好的图片,在下一轮中增加一些权重。'''
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') # 使用加权图像选择进行训练
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') # cuda 设备,即 0 或 0,1,2,3 或 cpu
'''--multi-scale :是否启用多尺度训练,默认是不开启的。'''
'''
多尺度训练是指设置几种不同的图片输入尺度,训练时每隔一定iterations随机选取一种尺度训练,这样训练出来的模型鲁棒性更强。多尺度训练在比赛中经常可以看到他身影,是被证明了有效提高性能的方式。
输入图片的尺寸对检测模型的性能影响很大,在基础网络部分常常会生成比原图小数十倍的特征图,导致小物体的特征描述不容易被检测网络捕捉。
通过输入更大、更多尺寸的图片进行训练,能够在一定程度上提高检测模型对物体大小的鲁棒性。
'''
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') # 图像大小变化 +/- 50%%
'''--single-cls :设定训练数据集是单类别还是多类别;默认为False多类别。'''
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class') # 将多类数据训练为单类
'''--optimizer :选择优化器;默认为SGD,可选SGD,Adam,AdamW。'''
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer') # 优化器
'''--sync-bn :是否开启跨卡同步BN;开启参数后即可使用SyncBatchNorm多 GPU 进行分布式训练。'''
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode') # 使用 SyncBatchNorm,仅在 DDP 模式下可用
'''--workers :最大worker数量;这里经常出问题,Windows系统报错时可以设置成 0 0 0。'''
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') # 最大数据加载器(DDP 模式下每个 RANK)
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name') # 保存到项目/名称
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 :官方发布的开启这个功能后的实际效果:好处是在比默认640大的数据集上训练效果更好。副作用是在640大小的数据集上训练效果可能会差一些。'''
parser.add_argument('--quad', action='store_true', help='quad dataloader') # 四路数据加载器
'''--cos-lr :是否开启余弦学习率;'''
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler') # 余弦学习率( LR )调度器
'''--label-smoothing :是否对标签进行平滑处理,默认是不启用的。'''
'''
在训练样本中,我们并不能保证所有sample都标注正确,如果某个样本标注错误,就可能产生负面印象,如果我们有办法“告诉”模型,样本的标签不一定正确,
那么训练出来的模型对于少量的样本错误就会有“免疫力”采用随机化的标签作为训练数据时,
损失函数有1-ε的概率与上面的式子相同,比如说告诉模型只有0.95概率是那个标签。
'''
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon') # 标签平滑 epsilon
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)') # EarlyStopping 耐心(无改进的时期)
'''--freeze :指定冻结层数量;可以在yolov5s.yaml中查看主干网络层数。'''
'''
冻结训练
是迁移学习常用的方法,当我们在使用数据量不足的情况下,通常我们会选择公共数据集提供权重作为预训练权重,
我们知道网络的backbone主要是用来提取特征用的,一般大型数据集训练好的权重主干特征提取能力是比较强的,
这个时候我们只需要冻结主干网络,fine-tune后面层就可以了,不需要从头开始训练,大大减少了实践而且还提高了性能。
'''
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2') # 冻结层:backbone=10,first3=0 1 2
'''--save-period :用于设置多少个epoch保存一下checkpoint'''
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') # 每 x 个时期保存一次检查点(如果 < 1 则禁用)
'''--seed :这是v6.2版本更新的一个非常重要的参数,如果你使用torch>=1.12.0的单GPU训练, 那你的训练结果完全可再现。'''
parser.add_argument('--seed', type=int, default=0, help='Global training seed') # 全球训练种子
'''--local_rank :DistributedDataParallel 单机多卡训练,单GPU设备不需要设置。'''
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') # 自动DDP多GPU参数,请勿修改
# Logger arguments
'''--entity :在线可视化工具,类似于tensorboard。'''
parser.add_argument('--entity', default=None, help='Entity') # 实体
'''--upload_dataset :是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集) 默认False。'''
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option') # 上传数据,“val”选项
'''--bbox_interval :设置界框图像记录间隔,默认 -1 。'''
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval') # 设置边界框图像记录间隔
parser.add_argument('--artifact_alias', type=str, default='latest', help='Version of dataset artifact to use') # 要使用的数据集工件的版本
# 如果在解析命令行参数时出现错误,该方法会抛出异常,导致程序无法继续执行。
# 为了避免这种情况,可以使用 parse_known_args() 方法,该方法与 parse_args() 方法类似,但是不会抛出异常,而是忽略未知的参数。
# parse_known_args() 方法返回两个值:args 和 unknown_args。args 是一个命名空间对象,包含解析出来的参数值;unknown_args 是一个列表,包含未解析的参数。
# 在程序中可以使用 args 对象来获取命令行参数的值,例如 args.input、args.output 和 args.verbose。在程序中也可以使用 unknown_args 列表来处理未知的参数。
'''返回解析的结果:如果参数 known 为 True,则调用 parse_known_args() 以解析已知参数;否则调用 parse_args() 解析所有参数。'''
return parser.parse_known_args()[0] if known else parser.parse_args()
4.def main(opt, callbacks=Callbacks()):
def main(opt, callbacks=Callbacks()):
# Checks 检查
'''检查分布式训练环境'''
'''# 检查当前的 RANK 值,并执行相关的检查操作。 如果 RANK 为 -1 或 0,执行以下检查。'''
'''
这段代码的作用是在特定条件下执行一些检查和打印操作。逐步分解如下:
1. 条件检查 (if RANK in {-1, 0}:):
RANK 是一个表示当前进程的排名的变量。通常在分布式训练中使用,多进程环境中每个进程会有一个唯一的排名。
{-1, 0} 代表的是两个特殊值:-1 表示是单进程训练,0 表示是主进程。在分布式训练中,通常只有主节点负责打印和代码检查工作,因此这里的条件用于确保只有主进程执行后续操作。
2. 打印参数 (print_args(vars(opt))):
vars(opt) 是将 opt 对象(通常包含命令行参数和配置选项)转换为字典格式,以便后续操作获取其属性。
print_args 函数则负责将这些参数以某种格式输出到控制台。这通常用于调试目的,帮助开发者检查和确认当前训练的配置。
3. 检查 Git 状态 (check_git_status()):
该函数用于检查当前代码库的 Git 状态(例如,是否有未提交的更改、当前分支信息等)。
它的目的是确保当前代码的版本是干净的,通常在开始训练之前检查代码状态,以避免意外的代码变动影响实验结果。
4. 检查依赖 (check_requirements()):
该函数会检查项目所需的 Python 包和库是否正确安装,并确保其版本满足要求。
目的是避免因缺少依赖或版本不兼容导致的运行时错误。
这段代码主要功能是在训练前进行必要的环境和配置检查。它确保只有主进程执行这些检查,以避免在分布式训练中重复冗余的操作。
同时通过打印参数、检查 Git 状态和验证依赖,帮助开发者确认训练环境的正确性,从而提高训练的可靠性和有效性。
'''
if RANK in {-1, 0}:
print_args(vars(opt)) # print_args() -> 打印函数参数(可选参数字典)
check_git_status() # check_git_status() -> YOLOv5 状态检查,如果代码过期,建议使用“git pull”
check_requirements() # check_requirements() -> 检查已安装的依赖项是否满足 YOLOv5 要求(传递 *.txt 文件或包列表或单个包 str)
'''
这段代码主要是关于断点训练的判断和准备。
断点训练是当训练异常终止或想调节超参数时,系统会保留训练异常终止前的超参数与训练参数,当下次训练开始时,并不会从头开始,而是从上次中断的地方继续训练。
使用断点续训,就从last.pt中读取相关参数。
不使用断点续训,就从文件中读取相关参数。
'''
'''
这段代码的主要功能是处理模型训练的恢复逻辑和配置文件的加载。当用户选择从上一个训练中恢复时,代码会在配置文件和检查点之间进行选择,并确保所有必要的参数都是有效的。
如果不是恢复情境,代码会验证所有输入,并处理超参数的进化同时设置训练的保存路径。最终,它确保训练环境的配置是正确的,从而准备好开始训练过程。
'''
# Resume (from specified or most recent last.pt) 恢复(从指定或最近的 last.pt)
# evolve进化 x 代的超参数
# check_comet_resume() -> 根据模型检查点和记录的实验参数将运行参数恢复到其原始状态。
'''判断是否断点续训'''
'''
判断条件:
opt.resume 为真,表示用户希望从上次训练的检查点恢复训练。
not check_comet_resume(opt) 确保用户不是从 Comet.ml 这样的实验追踪工具中恢复。
not opt.evolve 检查用户没有选择进化超参数模式。
'''
if opt.resume and not check_comet_resume(opt) and not opt.evolve:
# check_file() -> 搜索/下载文件(如有必要)并返回路径
# get_latest_run() -> /runs 中最新的“last.pt”的路径(即 --resume from)
'''# isinstance()是否是已经知道的类型。 # 如果resume是True,则通过get_lastest_run()函数找到runs为文件夹中最近的权重文件last.pt'''
'''
获取最新检查点路径:
如果 opt.resume 是字符串类型,调用 check_file(opt.resume) 来检查该路径是否有效;如果不是,则调用 get_latest_run() 获取最近一次运行的检查点。
'''
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())
'''
获取训练选项配置:
这里设置了 opt_yaml 的路径,该路径指向训练选项的 YAML 配置文件。同时保存了原始数据集的路径。
'''
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml 训练选项 yaml
opt_data = opt.data # original dataset 原始数据集
'''# 判断是否为文件'''
'''
读取配置文件:
检查 opt_yaml 文件是否存在。如果存在,则以 YAML 格式读取文件内容;如果不存在,则从最近的检查点加载训练选项。
'''
if opt_yaml.is_file(): # isfile()函数返回一个布尔值,如果存在指定的文件,则返回True,否则返回False。
'''# opt.yaml是训练时的命令行参数文件'''
with open(opt_yaml, errors='ignore') as f:
d = yaml.safe_load(f)
else:
d = torch.load(last, map_location='cpu')['opt']
# argparse是Python的标准库,用于解析命令行参数。它允许用户在运行程序时通过命令行传递参数,比如`--dataset_name`,`--model_name`等,而不是硬编码在代码中。
# Namespace 对象用来存储这些参数,可以像字典一样访问。尽管可以直接使用字典,但argparse提供了结构化和帮助信息的功能,使得参数管理和使用更加方便,特别是在大型项目中。
'''# 超参数替换,将训练时的命令行参数加载进opt参数对象中'''
'''
更新训练参数:
将读取到的训练参数更新到 opt 中,同时将 cfg 置为空,将 weights 设置为最新检查点的路径,并将 resume 设置为 True,表示即将恢复训练。
'''
opt = argparse.Namespace(**d) # replace 替换
'''# opt.cfg设置为'' 对应着train函数里面的操作(加载权重时是否加载权重里的anchor)'''
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate 恢复
'''
处理数据集路径:
如果 opt.data 是 URL,调用 check_file(opt_data) 来确保其有效性。
'''
if is_url(opt_data):
# check_file() -> 搜索/下载文件(如有必要)并返回路径
opt.data = check_file(opt_data) # avoid HUB resume auth timeout 避免 HUB 恢复身份验证超时
else:
'''# 不使用断点续训,就从文件中读取相关参数。 # check_file(utils/general.py)的作用为查找/下载文件 并返回该文件的路径。'''
'''如果不进行恢复,则对数据集和模型配置文件等进行有效性检查。'''
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 检查
# 必须指定 --cfg 或 --weights
'''# 如果模型文件和权重文件为空,弹出警告'''
'''
确保提供必要的配置或权重:
确保用户至少提供了模型配置或权重文件。
'''
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' # 必须指定 --cfg 或 --weights
'''# 如果要进行超参数进化,重建保存路径'''
'''
处理超参数进化的情况:
如果用户选择了超参数进化,则将项目路径更新为 runs/evolve 并设置 resume 为 False,以便不在进化过程中恢复训练。
'''
if opt.evolve:
'''# 设置新的项目输出目录'''
if opt.project == str(ROOT / 'runs/train'): # if default project name, rename to runs/evolve 如果是默认项目名称,则重命名为 runs/evolve
opt.project = str(ROOT / 'runs/evolve')
'''# 将resume传递给exist_ok'''
opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume 将 resume 传递给 exist_ok 并禁用 resume
'''
设置保存目录和名称:
如果名称是 cfg,则使用配置文件的基本名称。然后设置保存目录,确保如果目录已经存在则自动递增编号。
'''
if opt.name == 'cfg':
opt.name = Path(opt.cfg).stem # use model.yaml as name 使用 model.yaml 作为名称
# increment_path() -> 增加文件或目录路径,即 runs/exp --> runs/exp{sep}2, runs/exp{sep}3, ... 等等。
'''# 根据opt.project生成目录,并赋值给opt.save_dir 如: runs/train/exp1'''
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))
'''这段代码的主要功能是设置和初始化分布式数据并行 (Distributed Data Parallel, DDP) 的训练环境,尤其是在多GPU的情况下。这是深度学习训练中的一种常用方式,目的是提高训练速度和效率。'''
# DDP mode
'''支持多机多卡、分布式训练'''
'''# 选择程序装载的位置'''
device = select_device(opt.device, batch_size=opt.batch_size)
'''# 当进程内的GPU编号不为-1时,才会进入DDP'''
'''LOCAL_RANK 的值用于指示当前进程的排名。如果值为 -1,表示当前进程不是分布式训练中的一部分;如果不等于 -1,则表示这是一个分布式进程。'''
if LOCAL_RANK != -1:
msg = 'is not compatible with YOLOv5 Multi-GPU DDP training'
'''
这些 assert 语句用于检查一些基本条件,确保在分布式训练中使用的参数有效:
确保当前设备数量大于进程的 LOCAL_RANK,以防止在没有足够设备的情况下崩溃。
确保批量大小能够被世界大小(WORLD_SIZE)整除,即在所有进程间均匀分配。
确保批量大小不为 -1,因为 AutoBatch 需要一个有效的批次大小。
确保不会选择超参数进化(--evolve)。
确保没有启用图像权重(--image-weights)选项,因为在DDP训练中不兼容。
'''
'''# 不能使用图片采样策略'''
assert not opt.image_weights, f'--image-weights {msg}'
'''# 不能使用超参数进化'''
assert not opt.evolve, f'--evolve {msg}'
assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size'
'''# WORLD_SIZE表示全局的进程数'''
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE'
'''# 用于DDP训练的GPU数量不足'''
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command'
'''# 设置装载程序设备'''
torch.cuda.set_device(LOCAL_RANK)
'''# 保存装载程序的设备'''
'''
根据 LOCAL_RANK 设置当前CUDA设备,以确保此进程利用正确的GPU进行训练。
torch.device 用于创建一个设备对象,表示将要使用的 GPU。
'''
device = torch.device('cuda', LOCAL_RANK)
'''# torch.distributed是用于多GPU训练的模块'''
dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
'''这段代码是一个用于训练YOLOv5模型的部分,主要功能是进行模型训练和超参数的演化(进化)。'''
'''
超参数进化的步骤如下:
1.若存在evolve.csv文件,读取文件中的训练数据,选择超参进化方式,结果最优的训练数据突变超参数
2.限制超参进化参数hyp在规定范围内
3.使用突变后的超参数进行训练,测试其效果
4.训练结束后,将训练结果可视化,输出保存信息保存至evolution.csv,用于下一次的超参数突变。
原理:根据生物进化,优胜劣汰,适者生存的原则,每次迭代都会保存更优秀的结果,直至迭代结束。最后的结果即为最优的超参数
注意:使用超参数进化时要经过至少300次迭代,每次迭代都会经过一次完整的训练。因此超参数进化及其耗时,大家需要根据自己需求慎用。
这段代码主要是实现YOLOv5模型的训练与超参数的演化。其主要功能包括:
在指定条件下进行模型训练,或者基于已有的超参数进行演化。
通过定义、变异和选择超参数来优化模型性能,这个过程涵盖了数据的选择、变异概率控制及超参数限制等元素。
通过将演化过程可视化,帮助研究者理解超参数对模型性能的影响,从而进行更合理的超参数配置。
'''
# Train
# evolve进化 x 代的超参数
'''# Train 训练模式: 如果不进行超参数进化,则直接调用train()函数,开始训练。'''
'''# 如果不使用超参数进化'''
'''判断是否进行超参数演化:如果不进行超参数演化,则调用train函数进行模型训练,传入超参数、选项、设备和回调。'''
if not opt.evolve: # 如果 不是 进化x代的超参数
'''# 开始训练'''
train(opt.hyp, opt, device, callbacks)
# Evolve hyperparameters (optional) 进化超参数(可选)
'''# Evolve hyperparameters (optional) 遗传进化算法,边进化边训练'''
else: # 如果 是 进化x代的超参数
'''
如果选择进行演化,则定义一个字典meta,该字典记录了多个超参数的基本信息,包括:
超参数的名称
初始值
最小和最大值
'''
# Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit)
# 超参数演化元数据(突变尺度 0-1、lower_limit、upper_limit)
'''# 超参数列表(突变范围 - 最小值 - 最大值)'''
meta = {
'lr0': (1, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3) 初始学习率(SGD=1E-2,Adam=1E-3)
'lrf': (1, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf) 最终的 OneCycleLR 学习率 (lr0 * lrf)
'momentum': (0.3, 0.6, 0.98), # SGD momentum/Adam beta1 SGD 动量/Adam beta1
'weight_decay': (1, 0.0, 0.001), # optimizer weight decay 优化器权重衰减
'warmup_epochs': (1, 0.0, 5.0), # warmup epochs (fractions ok) 预热世代(epochs)(分数可以)
'warmup_momentum': (1, 0.0, 0.95), # warmup initial momentum 预热初始动量
'warmup_bias_lr': (1, 0.0, 0.2), # warmup initial bias lr 预热初始偏差学习率(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 分类 BCELoss 正权重
'obj': (1, 0.2, 4.0), # obj loss gain (scale with pixels) 目标 损失增益(按像素缩放)
'obj_pw': (1, 0.5, 2.0), # obj BCELoss positive_weight 目标 BCELoss 正权重
'iou_t': (0, 0.1, 0.7), # IoU training threshold IoU 训练阈值
'anchor_t': (1, 2.0, 8.0), # anchor-multiple threshold 锚框多重阈值
'anchors': (2, 2.0, 10.0), # anchors per output grid (0 to ignore) 每个输出网格的锚框(0 表示忽略)
'fl_gamma': (0, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5) 焦点损失 gamma(efficientDet默认 gamma=1.5)
'hsv_h': (1, 0.0, 0.1), # image HSV-Hue augmentation (fraction) 图像 HSV-色调增强(分数)
'hsv_s': (1, 0.0, 0.9), # image HSV-Saturation augmentation (fraction) 图像 HSV-饱和度增强(分数)
'hsv_v': (1, 0.0, 0.9), # image HSV-Value augmentation (fraction) 图像 HSV 值增强(分数)
'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 图像透视(+/- 分数),范围 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) 片段复制粘贴(概率)
'''# 加载默认超参数'''
'''读取初始超参数:读取位于opt.hyp路径的超参数配置文件,并将其解析为字典。如果超参数中没有anchors,则设置默认值为3。'''
with open(opt.hyp, errors='ignore') as f:
hyp = yaml.safe_load(f) # load hyps dict 加载 hyps 字典
'''# 如果超参数文件中没有'anchors',则设为3'''
if 'anchors' not in hyp: # anchors commented in hyp.yaml hyp.yaml 中评论的锚框
hyp['anchors'] = 3
'''如果不使用自动锚点,则删除锚点相关的超参数。'''
if opt.noautoanchor:
del hyp['anchors'], meta['anchors']
'''# 使用进化算法时,仅在最后的epoch测试和保存'''
'''准备演化的结果保存路径:设定了一些选项,比如只进行验证和只保存最终模型。同时定义了演化结果的保存路径,并且如果指定了存储桶,则从云端下载现有的evolve.csv文件。'''
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch 仅 测试/保存最终 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 {evolve_csv}') # download evolve.csv if exists 如果存在,请下载 evolve.csv
'''
遗传算法
调参:遵循适者生存、优胜劣汰的法则,即寻优过程中保留有用的,去除无用的。
遗传算法需要提前设置4个参数: 群体大小/进化代数/交叉概率/变异概率。
'''
'''# 选择超参数的遗传迭代次数 默认为迭代300次'''
'''开始进行多代的超参数演化,循环次数为opt.evolve的值。'''
for _ in range(opt.evolve): # generations to evolve 世代进化
'''# 如果evolve.csv文件存在'''
if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate 如果 evolve.csv 存在:选择最佳 hyps 并进行变异
# Select parent(s) 选择方式
'''# 选择超参进化方式,只用single和weighted两种'''
parent = 'single' # parent selection method: 'single' or 'weighted' 选择方法:“单一”或“加权”
# loadtxt(fname, dtype=<class 'float'>, comments='#', delimiter=None, converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0)
# 用于从文本加载数据。文本文件中的每一行必须含有相同的数据。
# fname :要读取的文件、文件名、或生成器。
# dtype :数据类型,默认float。还可以控制每一列的数据类型和精度等信息。
# comments :注释。
# delimiter :分隔符,默认是空格。
# skiprows :跳过前几行读取,默认是0,必须是int整型。
# usecols :要读取哪些列,0是第一列。例如,usecols = (1,4,5)将提取第2,第5和第6列。默认读取所有列。
# unpack :如果为True,将分列读取。
'''# 加载evolve.txt'''
x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1)
'''# 选取至多前五次进化的结果'''
n = min(5, len(x)) # number of previous results to consider 要考虑的先前结果的数量
# fitness() -> 模型适应度作为指标的加权组合
'''# fitness()为x前四项加权 [P, R, mAP@0.5, mAP@0.5:0.95] # np.argsort只能从小到大排序, 添加负号实现从大到小排序, 算是排序的一个代码技巧'''
x = x[np.argsort(-fitness(x))][:n] # top n mutations 前 n 个突变
'''# 根据(mp, mr, map50, map)的加权和来作为权重计算hyp权重'''
w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0) 权重(总和 > 0)
'''# 根据不同进化方式获得 base hyp'''
if parent == 'single' or len(x) == 1:
'''# 根据权重的几率随机挑选适应度历史前5的其中一个'''
# x = x[random.randint(0, n - 1)] # random selection
x = x[random.choices(range(n), weights=w)[0]] # weighted selection 加权选择
elif parent == 'weighted':
'''# 对hyp乘上对应的权重融合层一个hpy, 再取平均(除以权重和)'''
x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination 加权组合
# Mutate 突变(超参数进化)
'''定义变异概率和变异标准差,并以当前时间作为随机数种子。接着生成变异增益,以便在后续变异中使用。'''
mp, s = 0.8, 0.2 # mutation probability, sigma 突变概率,sigma
npr = np.random
'''# 根据时间设置随机数种子'''
npr.seed(int(time.time()))
'''
# 获取突变初始值, 也就是meta三个值的第一个数据
# 三个数值分别对应着: 变异初始概率, 最低限值, 最大限值(mutation scale 0-1, lower_limit, upper_limit)
'''
g = np.array([meta[k][0] for k in hyp.keys()]) # gains 0-1
ng = len(meta)
'''# 确保至少其中有一个超参变异了'''
v = np.ones(ng)
'''# 设置突变'''
while all(v == 1): # mutate until a change occurs (prevent duplicates) 不断变异直到发生变化(防止重复)
# out = np.clip(a, a_min, a_max, out=None) 是一个截取函数,用于截取数组中小于或者大于某值的部分,并使得被截取部分等于固定值。
# 参数说明
# a : 输入的数组
# a_min: 限定的最小值 也可以是数组 如果为数组时 shape必须和a一样
# a_max:限定的最大值 也可以是数组 shape和a一样
# out:剪裁后的数组存入的数组
v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
'''# 将突变添加到base hyp上'''
for i, k in enumerate(hyp.keys()): # plt.hist(v.ravel(), 300)
hyp[k] = float(x[i + 7] * v[i]) # mutate
# Constrain to limits 限制
'''# Constrain to limits 限制hyp在规定范围内'''
'''约束超参数值:确保变异后的超参数在定义的最小值和最大值之间,并保留一定的有效数字。'''
for k, v in meta.items():
'''
# 这里的hyp是超参数配置文件对象
# 而这里的k和v是在元超参数中遍历出来的
# hyp的v是一个数,而元超参数的v是一个元组
'''
'''# 先限定最小值,选择二者之间的大值 ,这一步是为了防止hyp中的值过小'''
hyp[k] = max(hyp[k], v[1]) # lower limit 下限
'''# 再限定最大值,选择二者之间的小值'''
hyp[k] = min(hyp[k], v[2]) # upper limit 上限
'''# 四舍五入到小数点后五位'''
hyp[k] = round(hyp[k], 5) # significant digits 有效数字
# Train mutation 训练突变
'''# Train mutation 使用突变后的参超,测试其效果'''
'''调用train函数进行训练,并保存结果。这会逐代训练模型,评估模型性能,同时也会记录变异结果。'''
results = train(hyp.copy(), opt, device, callbacks)
# class Callbacks: -> 处理 YOLOv5 Hooks 的所有已注册回调
callbacks = Callbacks()
# Write mutation results 写入突变结果
'''
# 将结果写入results,并将对应的hyp写到evolve.txt,evolve.txt中每一行为一次进化的结果
# 每行前七个数字 (P, R, mAP, F1, test_losses(GIOU, obj, cls)) 之后为hyp
# 保存hyp到yaml文件
'''
keys = ('metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', 'val/box_loss',
'val/obj_loss', 'val/cls_loss')
print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket)
# Plot results 绘制结果
# plot_evolve() -> 制 evolve.csv hyp 进化结果
'''# Plot results 将结果可视化 / 输出保存信息'''
'''绘制演化成果:将演化过程中记录的超参数和结果图形化,便于可视化分析。'''
plot_evolve(evolve_csv)
# 超参数演化完成 {opt.evolve} 代
# 结果保存至 {colorstr('bold', save_dir)}
# 使用示例:$ python train.py --hyp {evolve_yaml}
LOGGER.info(f'Hyperparameter evolution finished {opt.evolve} generations\n'
f"Results saved to {colorstr('bold', save_dir)}\n"
f'Usage example: $ python train.py --hyp {evolve_yaml}')
5.def run(**kwargs):
'''这是一个名为 run 的函数。它接受可变关键字参数 **kwargs,允许调用者传入任意数量的关键字参数。'''
def run(**kwargs):
'''这行注释提供了函数的使用示例,说明如何调用 run 方法并传入一些参数,比如数据集配置 data、图像大小 imgsz 和权重文件 weights。'''
# Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolov5m.pt')
# 用法:导入训练;train.run(data='coco128.yaml',imgsz=320,weights='yolov5m.pt')
'''解析选项:调用 parse_opt 函数并传入 True 作为参数。这通常会解析程序的命令行选项,并将解析后的结果存储在 opt 变量中。opt 通常是一个包含所有选项及其值的命名空间。'''
opt = parse_opt(True)
for k, v in kwargs.items():
'''设置选项:这段代码遍历 kwargs 中的每一个键值对 (k, v)。setattr 函数用于将 opt 对象的属性 k 设置为值 v。这样,传入的关键字参数就会覆盖或增加 opt 中的相应设置。'''
setattr(opt, k, v) # setattr(object, name, value) 用于设置属性值,该属性不一定是存在的。 object -- 对象。 name -- 字符串,对象属性。 value -- 属性值。
'''主函数调用:调用 main 函数,并将更新后的 opt 作为参数传入。main 函数负责启动训练或其他主要功能。'''
main(opt)
'''返回选项:最后返回 opt,使调用者可以获得当前的配置选项。'''
return opt
'''
该函数的主要功能是:
提供一个方便的接口来运行 YOLOv5 的训练过程或其他相关逻辑。
通过 **kwargs 动态接收用户自定义的参数,允许灵活地覆盖解析的命令行选项。
最终将更新后的选项传递给 main 函数以启动实际的训练或推断过程。
返回当前的配置选项给调用者,以便后续操作或日志记录。这使得代码更具可复用性和灵活性。
此方法在调用时提供了易用性,使得使用者可以简单地传递参数而不需要过多关注底层的解析逻辑。
'''
6.if __name__ == "__main__":
if __name__ == "__main__":
opt = parse_opt()
main(opt)