train.py
train.py
目录
train.py
1.所需的库和模块
2.def train(hyp, opt, device, callbacks):
3.def parse_opt(known=False):
4.def main(opt, callbacks=Callbacks()):
5.def run(**kwargs):
6.if __name__ == "__main__":
1.所需的库和模块
import argparse
import math
import os
import random
import sys
import time
from copy import deepcopy
from datetime import datetime
from pathlib import Path
import numpy as np
import torch
import torch.distributed as dist
import torch.nn as nn
import yaml
from torch.optim import lr_scheduler
from tqdm import tqdm
# 这段代码的作用是将当前文件所在的目录添加到Python的系统路径中,以便可以导入该目录下的其他模块。
# __file__
# 在Python中, __file__ 是一个特殊的内置变量,它返回当前文件的路径。这个变量包含了当前执行文件的完整路径,包括文件名和扩展名。
# __file__ 是当Python脚本被直接运行时自动创建的,而不是被导入时。这意味着如果有一个模块被另一个脚本导入, __file__ 将不会指向你期望的模块文件,而是指向导入它的那个脚本。
# 使用 Path 对象获取当前文件的绝对路径,并将其赋值给变量 FILE 。
FILE = Path(__file__).resolve()
# 获取 FILE 的父目录(即当前文件所在的目录),并将其赋值给变量 ROOT 。
ROOT = FILE.parents[0] # root directory
# 检查 ROOT 是否已经在系统路径 sys.path 中。
if str(ROOT) not in sys.path:
# 如果 ROOT 不在系统路径中,则将其添加到系统路径的末尾。
sys.path.append(str(ROOT)) # add ROOT to PATH
# os.path.relpath(path, start=None)
# os.path.relpath() 是 Python 的 os.path 模块中的一个函数,用于计算两个路径之间的相对路径。
# 参数 :
# path : 要计算相对路径的目标路径。
# start : 可选参数,起始路径。如果未指定或为 None ,默认为当前工作目录。
# 返回值 :
# 返回一个字符串,表示从 start 路径到 path 路径的相对路径。
# 功能描述 :
# 这个函数返回一个路径字符串,它是从 start 路径到 path 路径的相对路径。如果 start 参数未指定,则使用当前工作目录作为起始路径。
# 使用场景 :
# 当你需要从一个特定的目录构建到另一个目录的路径时。
# 在创建文件或目录的快捷方式时,可能需要相对路径而不是绝对路径。
# 注意事项 :
# 如果 path 是 start 的子目录,或者 path 和 start 有共同的前缀,那么函数将返回一个指向 path 的相对路径。
# 如果 path 和 start 不在同一目录树中,函数将返回 path 的绝对路径。
# 在 Windows 上,路径分隔符是反斜杠 \ ,而在 Unix-like 系统上是正斜杠 / , os.path.relpath() 会根据操作系统使用正确的路径分隔符。
# 将 ROOT 转换为相对于当前工作目录的相对路径,并重新赋值给 ROOT 变量。
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
# 这段代码通常用于Python项目中,确保模块可以被正确导入,特别是当你的项目结构比较复杂或者模块不在Python的标准库路径中时。这样做可以避免导入模块时出现 ModuleNotFoundError 。
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_img_size,
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, one_flat_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_tal 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)
# 这段代码是用于在分布式训练环境中获取环境变量并初始化一些变量的Python代码。
# os.getenv(key, default=None)
# os.getenv() 是 Python 标准库 os 模块中的一个函数,用于获取环境变量的值。环境变量是操作系统级别的变量,通常用于配置程序的运行环境。
# 参数 :
# key : 要获取的环境变量的名称(字符串)。
# default : 可选参数,如果指定的环境变量不存在,则返回该默认值。默认为 None 。
# 返回值 :
# 返回指定环境变量的值(字符串),如果环境变量不存在且未提供默认值,则返回 None ;如果提供了默认值,则返回该默认值。
# 使用场景 :
# os.getenv() 常用于读取配置参数,例如数据库连接字符串、API 密钥等,这些参数可以在操作系统环境中设置,以便在代码中使用。
# 它可以用于根据环境变量的值来调整程序的行为,例如在开发和生产环境中使用不同的配置。
# 注意事项 :
# 使用 os.getenv() 读取环境变量时,返回值始终是字符串类型。如果环境变量的值是数字或其他类型,仍然会被转换为字符串。
# 如果需要获取所有环境变量,可以使用 os.environ ,它返回一个包含所有环境变量的字典。
# os.getenv() 是处理环境变量的便捷方法,常用于配置和管理程序的运行环境。
# 使用 os 模块的 getenv 函数来获取环境变量 LOCAL_RANK 的值,并将其转换为整数。如果环境变量 LOCAL_RANK 不存在,则默认值为 -1 。 LOCAL_RANK 通常用于标识当前进程在其所在的物理设备(如GPU)上的排名。
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
# 使用 os.getenv 来获取环境变量 RANK 的值,并将其转换为整数。如果环境变量 RANK 不存在,则默认值为 -1 。 RANK 通常用于标识当前进程在整个分布式系统中的全局排名。
RANK = int(os.getenv('RANK', -1))
# 获取环境变量 WORLD_SIZE 的值,并将其转换为整数。如果环境变量 WORLD_SIZE 不存在,则默认值为 1 。 WORLD_SIZE 通常表示分布式系统中的总进程数或设备数。
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))
# 初始化了一个名为 GIT_INFO 的变量,并将其设置为 None 。这个变量用于存储有关Git仓库的信息,比如提交哈希、分支名称等,但在这段代码中并未进行赋值。
GIT_INFO = None
# 这些变量通常在分布式训练框架中使用,如PyTorch的 torch.distributed 或TensorFlow的 tf.distribute 。它们帮助框架理解当前进程的角色和位置,以及如何正确地初始化和同步分布式训练过程。
2.def train(hyp, opt, device, callbacks):
# 这段代码定义了一个名为 train 的函数,它用于执行深度学习模型的训练过程。主要功能是训练一个目标检测模型,并且包含了模型的初始化、数据加载、训练循环、验证和保存模型等步骤。
# 定义了一个名为 train 的函数,该函数用于启动和执行深度学习模型的训练过程。函数接受四个参数。
# 1.hyp :超参数,可以是包含超参数的字典,或者是指向包含超参数的 YAML 文件的路径。
# 2.opt :选项,通常是一个配置对象,包含训练过程中的各种设置,如学习率、批次大小、训练周期数等。
# 3.device :训练过程中使用的设备,通常是 GPU 或 CPU。
# 4.callbacks :回调函数列表,用于在训练的不同阶段执行特定的操作,如在每个周期开始和结束时记录日志、保存模型等。
def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary
# 这段代码是 train 函数的开始部分,它从 opt 对象中提取了一系列配置参数,并为训练过程的开始准备了一些变量。
# 这一行是多变量赋值的开始,它将 opt 对象中的一系列属性值赋给新的变量,以便于在训练过程中使用。这些变量包括保存目录、训练周期数、批次大小、预训练权重路径、是否为单类别模型、是否使用进化算法、数据集路径、模型配置、是否从断点恢复、是否跳过验证、是否不保存模型、工作线程数和冻结层参数。
# save_dir = Path(opt.save_dir)
# save_dir :这是一个 pathlib.Path 对象,它表示模型权重和训练结果的保存目录。 Path 是 pathlib 模块中的一个类,用于表示文件系统路径。
# epochs = opt.epochs
# epochs :这是一个整数,表示训练过程中的总周期数(epochs)。每个周期都会遍历一次完整的训练数据集。
# batch_size = opt.batch_size
# batch_size :这是一个整数,表示在训练过程中每个批次(batch)中的样本数量。
# weights = opt.weights
# weights :这是一个字符串,表示预训练模型权重的文件路径。这些权重可以在训练开始时用于初始化模型,或者在训练中断后用于恢复训练。
# single_cls = opt.single_cls
# single_cls :这是一个布尔值,表示是否为单类别(single-class)模型。如果是单类别模型,模型只会预测一个类别。
# evolve = opt.evolve
# evolve :这是一个布尔值,表示是否使用进化算法来优化超参数。
# data = opt.data
# data :这是一个字符串或字符串列表,表示训练和验证数据集的路径。
# cfg = opt.cfg
# cfg :这是一个字符串,表示模型配置文件的路径。这个文件通常包含了模型架构的详细信息。
# resume = opt.resume
# resume :这是一个布尔值,表示是否从之前保存的检查点(checkpoint)恢复训练。
# noval = opt.noval
# noval :这是一个布尔值,表示是否跳过验证步骤。如果设置为 True ,则不会在每个训练周期后对模型进行验证。
# nosave = opt.nosave
# nosave :这是一个布尔值,表示是否不保存模型权重和训练结果。
# workers = opt.workers
# workers :这是一个整数,表示用于数据加载的工作线程数。增加工作线程数可以提高数据加载的效率。
# freeze = opt.freeze
# 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 方法,并传入 'on_pretrain_routine_start' 事件,这通常用于在训练开始前执行一些预处理操作,如日志记录、环境检查等。
callbacks.run('on_pretrain_routine_start')
# 这段代码的主要目的是从 opt 对象中提取训练配置,并初始化训练环境。它设置了模型保存位置、训练周期数、批次大小等关键参数,并准备了回调函数以在训练的不同阶段执行特定操作。这些参数和设置将直接影响训练过程和结果,因此它们的正确设置对于模型训练至关重要。
# 这段代码是设置和管理模型训练过程中涉及的文件和目录的部分。
# Directories
# 定义了一个名为 w 的变量,它表示保存模型权重的目录。这个目录是 save_dir 下的一个子目录,名为 weights 。使用 / 操作符可以方便地构造路径。
w = save_dir / 'weights' # weights dir
# 创建了必要的目录。如果 evolve 是 True ,则创建 w 的父目录(即 save_dir );如果 evolve 是 False ,则直接创建 w 目录。 parents=True 表示创建父目录, exist_ok=True 表示如果目录已存在则不抛出异常。
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir
# 定义了 last 和 best 变量,它们分别指向保存最新模型权重和最佳模型权重的文件路径。 last.pt 和 best.pt 是文件名,它们都位于 w 目录下。
last, best = w / 'last.pt', w / 'best.pt'
# 定义了 last_striped 和 best_striped 变量,它们分别指向保存最新模型权重(带条纹)和最佳模型权重(带条纹)的文件路径。 last_striped.pt 和 best_striped.pt 是文件名,它们也位于 w 目录下。
last_striped, best_striped = w / 'last_striped.pt', w / 'best_striped.pt'
# 这段代码主要负责设置模型权重文件的存储目录,并确保这些目录存在。它还定义了保存最新模型和最佳模型权重的文件路径。这些文件将在整个训练过程中被更新和使用。通过这种方式,代码实现了模型权重的管理和存储,使得训练过程中的关键状态可以被保存和恢复。
# 这段代码处理超参数(hyperparameters)的加载和设置。
# Hyperparameters
# 检查 hyp 参数是否为字符串类型。如果是字符串,那么它通常表示一个文件路径。
if isinstance(hyp, str):
# 如果 hyp 是字符串,打开对应的文件。 errors='ignore' 参数意味着在文件打开过程中忽略任何编码错误。
with open(hyp, errors='ignore') as f:
# 使用 yaml.safe_load 函数从打开的文件中加载 YAML 格式的超参数。 safe_load 会将 YAML 内容解析为 Python 字典(dict),并将其赋值回 hyp 变量。
hyp = yaml.safe_load(f) # load hyps dict
# 记录超参数的信息。 LOGGER.info 是一个日志记录函数,用于输出信息到日志。 colorstr 函数用于给日志信息添加颜色。 ', '.join(f'{k}={v}' for k, v in hyp.items()) 将超参数字典转换为字符串,格式为 key=value ,并将它们以逗号分隔。
# def colorstr(*input): -> 构建并返回最终的字符串。它首先通过列表推导式和 join 函数将所有颜色和样式的 ANSI 代码连接起来,然后加上要着色的字符串 string ,最后加上 colors['end'] 来重置样式,确保之后的输出不会受到颜色代码的影响。 -> return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
# 在超参数字典中设置或更新 'anchor_t' 的值为 5.0 。 'anchor_t' 与目标检测中的锚点(anchor)阈值有关。
hyp['anchor_t'] = 5.0
# 将超参数字典的副本赋值给 opt.hyp 。这样做的目的是为了在保存训练检查点(checkpoints)时能够包含超参数信息。 hyp.copy() 确保了 opt 对象中的超参数是独立的副本,不会在后续过程中被意外修改。
opt.hyp = hyp.copy() # for saving hyps to checkpoints
# 这段代码负责从 YAML 文件中加载超参数,如果 hyp 是字典,则直接使用。然后,它记录超参数的详细信息,设置特定的超参数值,并为保存检查点做准备。通过这种方式,代码确保了超参数的正确加载和记录,这对于后续的训练过程和模型性能至关重要。
# 这段代码负责在训练开始前保存运行设置,包括超参数( hyp )和选项( opt )。
# Save run settings
# 检查 evolve 变量的值。如果 evolve 为 False ,则执行后面的代码块。 evolve 指的是是否使用进化策略来优化超参数,如果不是使用进化策略,那么需要保存当前的设置。
if not evolve:
# 调用 yaml_save 函数,将超参数字典 hyp 保存到 save_dir 目录下的 hyp.yaml 文件中。 save_dir / 'hyp.yaml' 构造了完整的文件路径。这个文件将包含所有重要的超参数,以便将来可以复现实验或进一步分析。
# def yaml_save(file='data.yaml', data={}): -> 将 Python 字典以 YAML 格式安全地保存到文件中。
yaml_save(save_dir / 'hyp.yaml', hyp)
# vars(object)
# vars() 函数在 Python 中用于获取对象的属性字典。这个字典包含了对象的大部分属性,但不包括方法和其他一些特殊的属性。对于用户自定义的对象, vars() 返回的字典包含了对象的 __dict__ 属性,这是一个包含对象所有属性的字典。
# 参数说明 :
# object :要获取属性字典的对象。
# 返回值 :
# 返回指定对象的属性字典。
# 注意事项 :
# vars() 对于内置类型(如 int 、 float 、 list 等)返回的是一个包含魔术方法和特殊属性的字典,这些属性通常是不可访问的。
# 对于自定义对象, vars() 返回的是对象的 __dict__ 属性,如果对象没有定义 __dict__ ,则可能返回一个空字典或者抛出 TypeError 。
# 在 Python 3 中, vars() 也可以用于获取内置函数的全局变量字典。
# vars() 函数是一个内置函数,通常用于调试和访问对象的内部状态,但在处理复杂对象时应该谨慎使用,因为直接修改对象的属性可能会导致不可预测的行为。
# 同样调用 yaml_save 函数,将选项对象 opt 转换为字典(使用 vars 函数),并保存到 save_dir 目录下的 opt.yaml 文件中。这个文件将包含训练过程中用到的所有配置选项。
yaml_save(save_dir / 'opt.yaml', vars(opt))
# 这段代码的目的是确保在训练开始之前,所有的超参数和配置选项都被保存下来。通过将这些设置保存为 YAML 文件,可以方便地记录和追踪实验的配置,这对于实验的可重复性和透明度是非常重要的。如果训练过程中断,这些文件也可以帮助研究人员从断点恢复训练。
# 这段代码涉及到日志记录器(loggers)的初始化和配置,以及注册回调函数和处理远程数据集的逻辑。
# Loggers
# 初始化了一个变量 data_dict ,它将用于存储与数据集相关的信息。
data_dict = None
# 检查 RANK 变量的值。 RANK 通常用于分布式训练中标识当前进程的编号。 -1 表示非分布式训练环境, 0 表示分布式训练中的第一个进程。如果 RANK 是这两个值之一,那么执行后面的代码块。
if RANK in {-1, 0}:
# 创建了一个 Loggers 类的实例,传入保存目录、权重路径、选项对象、超参数字典和日志记录器。 Loggers 类负责管理训练过程中的各种日志记录任务。
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance
# Register actions
# 遍历 loggers 实例中所有可用的方法。
# def methods(instance): -> 获取一个类实例的所有可调用方法。使用列表推导式来构建一个方法名的列表。 -> return [f for f in dir(instance) if callable(getattr(instance, f)) and not f.startswith("__")]
for k in methods(loggers):
# 对于每个方法 k ,使用 getattr 函数获取 loggers 实例对应的方法,并将其注册为回调函数,键为方法名 k 。这意味着在训练过程中的特定事件(如每个周期开始和结束)时,这些方法将被自动调用。
callbacks.register_action(k, callback=getattr(loggers, k))
# Process custom dataset artifact link
# 从 loggers 实例中获取远程数据集的信息,并将其存储在 data_dict 变量中。这用于处理从远程存储或服务中获取的数据集。
data_dict = loggers.remote_dataset
# 检查是否需要从远程工件恢复训练。如果 resume 为 True ,则执行后面的代码块。
if resume: # If resuming runs from remote artifact
# 如果从远程工件恢复训练,将 opt 对象中的 权重路径 、 周期数 、 超参数 和 批次大小 重新赋值给相应的变量。这样做是为了确保在恢复训练时使用正确的配置。
weights, epochs, hyp, batch_size = opt.weights, opt.epochs, opt.hyp, opt.batch_size
# 这段代码的目的是设置训练过程中的日志记录,并注册相关的回调函数。它还处理了从远程数据源获取数据集的情况,并在恢复训练时确保使用正确的配置。通过这种方式,代码提高了训练过程的灵活性和可追踪性,使得研究人员可以更好地监控和分析训练过程。
# 这段代码涉及到配置训练过程中的一些关键设置,包括是否创建绘图、设备选择、随机种子初始化、数据集检查、以及确定类别数量和名称。
# Config
# 设置了一个布尔值 plots ,它决定了是否在训练过程中创建绘图。如果 evolve 是 False (不使用进化算法)且 opt.noplots 是 False (用户没有指定不创建绘图),则 plots 为 True 。
plots = not evolve and not opt.noplots # create plots
# 检查 device 对象的类型,如果设备不是 CPU,则 cuda 为 True ,表示将使用 CUDA(GPU加速)。
cuda = device.type != 'cpu'
# 初始化随机种子,以确保实验的可重复性。 opt.seed 是用户指定的种子值, 1 + RANK 是为了在分布式训练中为每个进程提供一个唯一的种子值。 deterministic=True 确保了 PyTorch 的某些操作(如卷积)是确定性的。
# def init_seeds(seed=0, deterministic=False): -> 在不同的库中初始化随机种子,以确保实验的可重复性。
init_seeds(opt.seed + 1 + RANK, deterministic=True)
# 使用 torch.distributed 的上下文管理器 torch_distributed_zero_first ,它确保在分布式训练中只有 LOCAL_RANK 为 0 的进程先执行某些操作。
# def torch_distributed_zero_first(local_rank: int): -> 这是一个装饰器,用于定义一个上下文管理器,它允许使用 with 语句来管理资源的获取和释放。使用这个上下文管理器的目的是确保在分布式训练中,所有进程在执行某些关键操作之前都等待主进程完成初始化或其他任务。这有助于避免数据不一致和潜在的竞态条件。
with torch_distributed_zero_first(LOCAL_RANK):
# 检查 data_dict 是否为空,如果为空,则调用 check_dataset 函数来检查数据集,并更新 data_dict 。
# def check_dataset(data, autodownload=True): ->检查、下载(如果需要)并解压数据集,确保数据集在本地可用。返回包含数据集信息的字典。 -> return data # dictionary
data_dict = data_dict or check_dataset(data) # check if None
# 从 data_dict 中提取训练路径和验证路径。
train_path, val_path = data_dict['train'], data_dict['val']
# 设置类别数量 nc 。如果是单类别模型,则 nc 为 1;否则,从 data_dict 中获取类别数量。
nc = 1 if single_cls else int(data_dict['nc']) # number of classes
# 设置类别名称。如果是单类别模型且 data_dict 中的名称列表不是单一元素,则 names 设置为 {0: 'item'} ;否则,直接使用 data_dict 中的名称列表。
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names
#is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset
# 检查验证路径 val_path 是否是字符串,并且以 'val2017.txt' 结尾,如果是,则认为这是一个 COCO 数据集。
is_coco = isinstance(val_path, str) and val_path.endswith('val2017.txt') # COCO dataset
# 这段代码配置了训练过程中的一些关键参数,包括绘图设置、设备选择、随机种子初始化、数据集检查和类别信息。这些设置对于确保训练的可重复性、正确性和监控训练进度非常重要。通过检查数据集和设置类别信息,代码还为后续的训练和验证步骤提供了必要的信息。
# 这段代码涉及到模型的加载和初始化,包括检查预训练权重、下载权重、加载模型、以及设置自动混合精度(AMP)。
# Model
# 检查提供的权重文件路径 weights 是否以 .pt 后缀结尾,这是一个常见的 PyTorch 模型权重文件格式。
# def check_suffix(file='yolo.pt', suffix=('.pt',), msg=''): -> 用于检查一个或多个文件的后缀是否符合指定的后缀列表。
check_suffix(weights, '.pt') # check weights
# 设置一个布尔值 pretrained ,如果 weights 以 .pt 结尾,则表示有预训练权重可用。
pretrained = weights.endswith('.pt')
# 检查是否有预训练权重可用。
if pretrained:
# 使用 torch.distributed 的上下文管理器,确保在分布式训练中只有 LOCAL_RANK 为 0 的进程执行以下代码。
# def torch_distributed_zero_first(local_rank: int): -> 这是一个装饰器,用于定义一个上下文管理器,它允许使用 with 语句来管理资源的获取和释放。使用这个上下文管理器的目的是确保在分布式训练中,所有进程在执行某些关键操作之前都等待主进程完成初始化或其他任务。这有助于避免数据不一致和潜在的竞态条件。
with torch_distributed_zero_first(LOCAL_RANK):
# 如果权重文件在本地未找到,尝试从远程下载。 attempt_download 函数会检查文件是否存在,如果不存在则尝试下载。
# def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'): -> 尝试从 GitHub 仓库的发布资产中下载文件,如果本地找不到该文件。返回文件的路径,以字符串形式。 -> return str(file)
weights = attempt_download(weights) # download if not found locally
# 加载预训练的权重文件到 CPU,以避免 CUDA 内存泄漏。这里使用 map_location='cpu' 确保权重被加载到 CPU 而不是 GPU。
ckpt = torch.load(weights, map_location='cpu') # load checkpoint to CPU to avoid CUDA memory leak
# 创建模型实例。如果提供了配置 cfg ,则使用该配置;如果没有提供,则使用预训练权重中的模型配置。 ch=3 表示输入通道数为 3(适用于 RGB 图像), nc=nc 设置类别数, anchors=hyp.get('anchors') 设置锚点。
# class DetectionModel(BaseModel):
# -> 用于构建和处理YOLO目标检测模型。
# -> def __init__(self, cfg='yolo.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
# 设置一个排除列表,如果配置中指定了锚点且不是从检查点恢复,则排除锚点键。
exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys
# 从预训练权重中获取模型的状态字典,并确保其为单精度浮点数(FP32)。
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32
# 将 预训练权重的状态字典 与 新模型的状态字典 相交,只保留匹配的键,忽略不匹配的键。
# def intersect_dicts(da, db, exclude=()): -> 计算两个字典 da 和 db 中具有匹配键和形状的元素的交集,同时排除指定的键。计算交集。 -> return {k: v for k, v in da.items() if k in db and all(x not in k for x in exclude) and v.shape == db[k].shape}
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect
# 加载预训练权重到模型中, strict=False 允许一些键不匹配,即忽略模型中未使用的预训练权重。
model.load_state_dict(csd, strict=False) # load
# 记录日志,报告从预训练权重文件中成功转移了多少项到模型中。
LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report 从 {weights} 转移了 {len(csd)}/{len(model.state_dict())} 个项目。
# 如果没有预训练权重可用,则使用提供的配置 cfg 创建一个新的模型实例。
else:
# 创建一个新的模型实例,并将其移动到指定的设备(CPU 或 GPU)。
# class DetectionModel(BaseModel):
# -> 用于构建和处理YOLO目标检测模型。
# -> def __init__(self, cfg='yolo.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
# 检查模型是否支持自动混合精度(AMP),这是一种用于加速训练并减少内存使用的混合精度训练技术。
# def check_amp(model): -> 用于检查 PyTorch 自动混合精度(AMP)功能是否在模型上正确工作。如果 AMP 功能正常,函数返回 True ;如果出现问题,则返回 False 并记录警告日志。 -> return True / return False
amp = check_amp(model) # check AMP
# 这段代码负责根据是否使用预训练权重来加载和初始化模型。如果使用预训练权重,它会下载权重、加载模型,并确保权重与模型架构匹配。无论是否使用预训练权重,最后都会检查模型是否支持 AMP,以便在训练中使用混合精度。这个过程确保了模型可以根据提供的配置和权重正确地初始化,为后续的训练做好准备。
# 这段代码处理模型中特定层的冻结操作,即设置这些层在训练过程中不被更新。
# Freeze
# 构造了一个列表 freeze ,其中包含了需要冻结的模型层的名称。如果 freeze 参数是一个列表且长度大于1,它直接使用这个列表;如果 freeze 只包含一个元素,它将这个元素视为起始值,并生成一个从0到该值的整数范围。每个元素都被格式化为字符串,以 model. 为前缀,表示模型中的层。
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze
# 遍历模型的所有参数, k 是参数的名称, v 是对应的参数对象。
for k, v in model.named_parameters():
# 这是一个注释掉的代码行,如果取消注释,它将设置所有层的 requires_grad 属性为 True ,意味着在训练过程中将更新所有层的参数。这是一个待办事项(TODO),提示在主分支(master)中应该取消注释这行代码。
# v.requires_grad = True # train all layers TODO: uncomment this line as in master
# torch.nan_to_num(input, nan=0.0, posinf=None, neginf=None, out=None) -> Tensor
# torch.nan_to_num() 是 PyTorch 提供的一个函数,它用于将张量中的 NaN (Not a Number)和 inf (无穷大)值替换为数值。这个函数对于处理数值计算中的异常值非常有用,尤其是在深度学习训练中,当出现梯度爆炸或梯度消失时,可能会导致 NaN 或 inf 值。
# 参数说明 :
# input :输入的张量,可能包含 NaN 和 inf 值。
# nan :一个数值,用来替换 input 中的 NaN 值,默认为 0.0 。
# posinf :一个数值,用来替换 input 中的正无穷大值 +inf ,默认为 None ,表示不替换。
# neginf :一个数值,用来替换 input 中的负无穷大值 -Inf ,默认为 None ,表示不替换。
# out :输出张量,如果提供,函数会将结果写入这个张量中,否则会创建一个新的张量作为输出。
# 返回值 :
# 返回一个新的张量,其中 input 中的 NaN 和 inf 值已经被替换。
# 这是另一个注释掉的代码行,如果取消注释,它将为每个参数注册一个钩子(hook),当参数值为 NaN(不是一个数字)时,将其转换为 0。这个钩子被注释掉是因为在某些情况下它可能导致训练结果不稳定。
# v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
# 检查参数名称 k 是否包含在冻结层列表 freeze 中的任何一个元素。
if any(x in k for x in freeze):
# 如果参数名称 k 包含在冻结层列表中,记录一条日志信息,表示正在冻结该层。
LOGGER.info(f'freezing {k}')
# 如果参数名称 k 包含在冻结层列表中,将对应参数的 requires_grad 属性设置为 False ,意味着在训练过程中不会更新这个参数。
v.requires_grad = False
# 这段代码的目的是冻结模型中的特定层,使得这些层在训练过程中保持不变。通过构造一个包含需要冻结层名称的列表,并遍历模型的所有参数,代码检查每个参数是否属于需要冻结的层。如果是,就将其 requires_grad 属性设置为 False 。这个过程确保了只有模型中未被冻结的层会在训练中被更新,而冻结的层则保持其初始状态。
# 这段代码涉及到图像尺寸的处理,特别是在模型训练中确保图像尺寸与模型的步长(stride)兼容。
# Image size
# 计算模型的最大步长(stride),并将其转换为整数。 model.stride.max() 获取模型中所有层步长的最大值。 gs 代表网格尺寸(grid size),它必须是模型步长的倍数,同时也不能小于32(一个常见的最小网格尺寸)。 max() 函数确保 gs 至少为32,即使模型的最大步长小于32。
gs = max(int(model.stride.max()), 32) # grid size (max stride)
# 调用 check_img_size 函数来验证或调整图像尺寸 imgsz ,确保它是 gs 的倍数。 opt.imgsz 是用户提供的原始图像尺寸,可能是一个整数或一对整数(分别代表宽度和高度)。 gs 是之前计算的网格尺寸。 floor=gs * 2 参数指定了图像尺寸的下限,即图像尺寸不能小于 gs 的两倍。如果用户提供的图像尺寸不符合要求, check_img_size 函数将调整它,使其成为 gs 的倍数,并且至少为 gs * 2 。
# def check_img_size(imgsz, s=32, floor=0): -> 验证图像尺寸是否是某个步长的倍数。返回新尺寸。函数返回新计算的尺寸。 -> return new_size
imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2) # verify imgsz is gs-multiple
# 这段代码的目的是确保图像尺寸适合模型的步长,并且满足一定的最小尺寸要求。通过这种方式,可以避免在训练过程中由于图像尺寸不匹配而导致的问题,如步长不一致或图像尺寸过小。这对于确保模型训练的稳定性和性能是非常重要的。
# 这段代码处理批量大小(batch size)的设置,特别是在单GPU训练环境中自动估计最佳的批量大小。
# Batch size
# 检查两个条件 : RANK 是否为 -1 (表示非分布式训练环境,即单GPU训练),以及 batch_size 是否为 -1 (表示用户没有指定批量大小,需要自动估计)。
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size
# 如果上述条件满足,调用 check_train_batch_size 函数来估计最佳的批量大小。这个函数基于模型结构、图像尺寸 imgsz 和是否使用自动混合精度(AMP)来决定适合当前硬件和模型的批量大小。
# def check_train_batch_size(model, imgsz=640, amp=True): -> 用于计算给定模型在特定图像尺寸下的最优批量大小(batch size)。 -> return autobatch(deepcopy(model).train(), imgsz) # compute optimal batch size
batch_size = check_train_batch_size(model, imgsz, amp)
# 通知日志记录器 loggers 更新参数。它传递一个字典,其中包含新的 batch_size 值,这样日志记录器可以记录下这个重要的训练参数。
loggers.on_params_update({"batch_size": batch_size})
# 这段代码的目的是在单GPU训练环境中,当用户没有指定批量大小时,自动估计一个合适的批量大小。这有助于优化训练性能,因为不同的模型和硬件配置可能需要不同的批量大小以达到最佳训练效果。通过自动调整批量大小,可以减少用户在训练配置上的负担,并可能提高训练的效率和稳定性。
# 这段代码涉及到优化器的设置,包括计算累积梯度的批次数、调整权重衰减以及初始化优化器。
# Optimizer
# 设置了一个名义批量大小 nbs ,通常是一个基准值,用于与其他批量大小进行比较。
nbs = 64 # nominal batch size
# 计算了在执行一次优化步骤之前需要累积的梯度批次数。这是通过将名义批量大小 nbs 除以实际批量大小 batch_size 来计算的,然后使用 round 函数四舍五入到最近的整数。 max 函数确保累积的最小批次数为1,即使 batch_size 大于 nbs 。
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
# 根据实际的批量大小和累积批次数调整超参数中的权重衰减 weight_decay 。权重衰减被乘以 (batch_size * accumulate) / nbs ,这是一个缩放因子,用于保持梯度累积和批量大小变化时权重衰减的相对强度。
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay
# 初始化优化器。 smart_optimizer 函数根据提供的参数创建并返回一个优化器实例。参数包括模型 model 、优化器类型 opt.optimizer 、初始学习率 hyp['lr0'] 、动量 hyp['momentum'] 和权重衰减 hyp['weight_decay'] 。
# def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5): -> 用于创建一个智能优化器,根据模型的不同参数分配不同的学习率和衰减率。返回创建的优化器实例。 -> return optimizer
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay'])
# 这段代码的目的是设置和初始化训练过程中使用的优化器。通过计算累积梯度的批次数和调整权重衰减,代码确保了即使在小批量训练时也能保持相对稳定的梯度更新强度。此外,通过 smart_optimizer 函数,可以根据超参数和用户选项灵活地选择和配置不同的优化器,这对于模型的训练效率和收敛性至关重要。
# 这段代码涉及到学习率调度器(scheduler)的设置,它根据不同的策略来调整训练过程中的学习率。
# Scheduler
# 判断是否使用余弦退火(cosine annealing)学习率调度策略。如果 opt.cos_lr 为 True ,则执行以下代码。
if opt.cos_lr:
# 初始化一个余弦退火学习率调度器,起始学习率为1(或100%),结束学习率为 hyp['lrf'] (超参数中定义的最终学习率比例),总周期数为 epochs 。
# def one_cycle(y1=0.0, y2=1.0, steps=100): -> 用于生成一个周期性的函数,该函数在给定的步数内从 y1 值平滑过渡到 y2 值,并再过渡回 y1 值。返回一个 lambda 函数,这个匿名函数接受一个参数 x ,表示当前的步数。 -> return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf']
# 判断是否使用平坦余弦退火(flat cosine annealing)学习率调度策略。如果 opt.flat_cos_lr 为 True ,则执行以下代码。
elif opt.flat_cos_lr:
# 初始化一个平坦余弦退火学习率调度器,与余弦退火类似,但在退火过程中保持一段时间的学习率不变。
# def one_flat_cycle(y1=0.0, y2=1.0, steps=100): -> 用于实现一个具有平坦峰值的周期性变化。这种周期性变化在到达中点后保持峰值,直到周期结束。 -> return lambda x: ((1 - math.cos((x - (steps // 2)) * math.pi / (steps // 2))) / 2) * (y2 - y1) + y1 if (x > (steps // 2)) else y1
lf = one_flat_cycle(1, hyp['lrf'], epochs) # flat cosine 1->hyp['lrf']
# 断是否使用固定学习率。如果 opt.fixed_lr 为 True ,则执行以下代码。
elif opt.fixed_lr:
# 定义了一个常数函数作为学习率调度器,无论训练到哪个周期,学习率都保持为1.0(或100%)。
lf = lambda x: 1.0
# 如果以上条件都不满足,则使用线性学习率调度策略。
else:
# 定义了一个线性递减的学习率调度器。随着训练周期 x 的增加,学习率从1(或100%)线性递减到 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)
# torch.optim.lr_scheduler.LambdaLR 是 PyTorch 中的一个学习率调度器,它允许你根据一个给定的函数来调整学习率。这个函数可以自定义,以适应不同的训练需求和策略。
# 参数 :
# optimizer :被包装的优化器。
# lr_lambda :一个函数,它接受一个整数参数 epoch ,并返回一个乘法因子。这个因子将用于调整学习率。也可以是一个函数列表,其中每个函数对应于优化器中的一个参数组。
# last_epoch :整数,表示最后一个epoch的索引。默认为 -1 ,意味着从初始学习率开始。
# verbose:布尔值,如果为 True ,则在每次更新时打印一条消息到标准输出。默认为 False 。
# 工作原理 :
# LambdaLR 调度器将每个参数组的学习率设置为初始学习率乘以给定函数的值。这个函数可以是任何形式,例如,可以是一个简单的线性衰减、指数衰减,或者更复杂的自定义函数。
# 使用 PyTorch 的 LambdaLR 类创建一个学习率调度器 scheduler 。 optimizer 是之前创建的优化器, lr_lambda 是上面定义的学习率调整函数 lf 。
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)
# from utils.plots import plot_lr_scheduler; plot_lr_scheduler(optimizer, scheduler, epochs)
# 这段代码的目的是根据用户的选择设置不同的学习率调整策略。通过定义不同的学习率函数 lf ,并将其传递给 LambdaLR 调度器,可以在训练过程中动态调整学习率。这有助于模型在训练初期快速收敛,在训练后期细化权重调整,从而达到更好的训练效果。
# 这段代码涉及到指数移动平均(Exponential Moving Average, EMA)的设置,这是一种用于稳定训练过程和提高模型泛化能力的技术。
# EMA
# 检查 RANK 变量的值。 RANK 通常用于分布式训练中标识当前进程的编号。 -1 表示非分布式训练环境, 0 表示分布式训练中的第一个进程。
# 如果 RANK 是 -1 或 0 ,这意味着代码正在单GPU环境或分布式训练的主进程中运行,因此会创建一个 ModelEMA 实例,并将当前的模型 model 传递给它。 ModelEMA 是一个用于计算模型参数的指数移动平均的类。
# 如果 RANK 不是 -1 或 0 ,这意味着代码不在单GPU环境或分布式训练的主进程中运行,因此不创建 ModelEMA 实例, ema 变量被设置为 None 。
# class ModelEMA:
# -> 用于实现模型的指数移动平均(Exponential Moving Average,简称 EMA)。EMA是一种技术,用于平滑模型参数,通常用于提高模型的稳定性和性能。
# -> def __init__(self, model, decay=0.9999, tau=2000, updates=0):
ema = ModelEMA(model) if RANK in {-1, 0} else None
# 这段代码的目的是在单GPU环境或分布式训练的主进程中初始化EMA。EMA是一种技术,它通过计算模型参数的指数加权平均来平滑模型的参数变化,有助于减少训练过程中的噪声,提高模型的稳定性和性能。在非主进程中不初始化EMA,是因为在分布式训练中,只有主进程需要保存和更新EMA参数,其他进程可以从主进程同步EMA参数。
# 这段代码处理模型训练的恢复逻辑,特别是在使用预训练权重的情况下。
# Resume
# 初始化两个变量 : best_fitness 和 start_epoch 。 best_fitness 用于存储训练过程中的最佳适应度(或性能指标), start_epoch 用于标记从哪个周期开始继续训练。这两个值都被初始化为0.0和0,分别对应于训练开始时的默认值。
best_fitness, start_epoch = 0.0, 0
# 断是否使用了预训练权重。如果 pretrained 为 True ,则执行以下代码块。
if pretrained:
# 在已经使用预训练权重的前提下,这个条件判断是否需要从之前的训练状态恢复。如果 resume 为 True ,则执行以下代码块。
if resume:
# 调用 smart_resume 函数,该函数负责从检查点(checkpoint)恢复训练状态。 smart_resume 函数接收以下参数 : ckpt 检查点文件的路径或已加载的检查点对象。 optimizer 当前的优化器实例。 ema 指数移动平均(EMA)实例,如果使用EMA的话。 weights 预训练权重的路径。 epochs 总训练周期数。 resume 是否从检查点恢复训练的布尔值。
# 函数返回三个值 : best_fitness (恢复时的最佳适应度)、 start_epoch (恢复时的起始周期)和更新后的 epochs (根据检查点中的信息调整)。
# def smart_resume(ckpt, optimizer, ema=None, weights='yolov5s.pt', epochs=300, resume=True): -> 用于从部分训练的检查点(checkpoint)恢复训练。返回 最佳适应度值 、 起始轮数 和 更新后的训练轮数 。 -> return best_fitness, start_epoch, epochs
best_fitness, start_epoch, epochs = smart_resume(ckpt, optimizer, ema, weights, epochs, resume)
# 删除 ckpt 和 csd 变量,它们分别指向检查点对象和检查点中的状态字典。删除这些变量可以释放内存,特别是在处理大型模型或检查点时。
del ckpt, csd
# 这段代码的目的是在训练过程中实现从预训练权重和检查点的恢复。通过 smart_resume 函数,可以从之前的训练状态中恢复最佳适应度、起始周期,并可能调整总周期数。这允许训练过程在中断后继续进行,而不是从头开始,有助于节省时间和资源,特别是在长时间训练的大型模型中。删除检查点变量有助于在恢复后清理内存。
# 这段代码处理的是模型在多GPU环境下的分布式数据并行(Data Parallel,简称DP)模式的设置。
# DP mode
# 这个条件语句检查三个条件 : cuda 是否使用CUDA,即是否在GPU上运行。 RANK == -1 表示当前进程不是分布式训练的一部分,即非分布式训练环境。 torch.cuda.device_count() > 1 检查系统中是否有多于一个的GPU设备。
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
# 如果上述条件满足,即在多GPU环境下但不是分布式训练环境,记录一条警告日志。这条警告建议用户不要使用数据并行(DP),而是使用分布式数据并行(Distributed Data Parallel,简称DDP)来获得更好的多GPU训练结果。这是因为DDP通常比DP更高效,特别是在大规模多GPU训练时。
LOGGER.warning('WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.') # 警告⚠️不推荐使用 DP,使用 torch.distributed.run 可获得最佳 DDP 多 GPU 效果。
# torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
# torch.nn.DataParallel 是 PyTorch 提供的一个模块,用于在多个GPU上并行执行模型的前向传播和反向传播,以加速训练过程。
# 参数说明 :
# module ( nn.Module ) :需要并行化的模型模块。
# device_ids ( list 或 tuple ,可选) :要使用的GPU设备的ID列表。如果不指定,则使用所有可用的GPU。
# output_device ( torch.device 或 int ,可选) :输出设备的ID。默认为 device_ids 中的第一个设备。
# dim ( int ,可选) :在输入张量中,输入数据的维度。默认为0。
# 工作方式 :
# DataParallel 通过将输入数据自动分割到指定的多个GPU上,并在每个GPU上并行执行模型的前向传播和反向传播,然后将结果收集并返回。在反向传播时,它也会自动将梯度收集并求和。
# 需要注意的是, DataParallel 并不支持模型内部的梯度累积,它只是简单地将输入数据分割,并在多个GPU上并行计算。对于更复杂的并行策略,如模型并行或分布式训练,可能需要使用其他工具,如 torch.distributed 或 torch.nn.parallel.DistributedDataParallel 。
# 如果条件满足,将模型包装在 torch.nn.DataParallel 中。 DataParallel 是PyTorch提供的一个包装器,它可以自动地将数据分发到多个GPU上,并收集结果。这样,即使在非分布式训练环境中,也可以利用多个GPU来加速训练。
model = torch.nn.DataParallel(model)
# 这段代码的目的是在多GPU环境下,当不是分布式训练时,使用数据并行来加速模型训练。然而,它也提醒用户,对于最佳的多GPU训练性能,应该使用分布式数据并行。通过将模型包装在 DataParallel 中,可以使得模型在多个GPU上并行运行,但需要注意,这种方式可能不如分布式训练高效。
# 这段代码涉及到在多GPU训练环境中同步批量归一化(Synchronized Batch Normalization, SyncBatchNorm)的设置。
# SyncBatchNorm
# 这个条件语句检查是否需要应用同步批量归一化(SyncBatchNorm)。它需要满足以下条件 : opt.sync_bn 用户选项中指定是否使用同步批量归一化。 cuda 是否使用CUDA,即是否在GPU上运行。 RANK != -1 表示当前进程是在分布式训练环境中, RANK 不为 -1 表示不是单GPU训练环境。
if opt.sync_bn and cuda and RANK != -1:
# torch.nn.SyncBatchNorm.convert_sync_batchnorm(module, process_group=None)
# torch.nn.SyncBatchNorm.convert_sync_batchnorm() 是 PyTorch 中的一个类方法,用于将模型中的所有 BatchNorm*D 层(例如 BatchNorm1d , BatchNorm2d , BatchNorm3d )转换为 SyncBatchNorm 层。这对于在分布式训练环境中同步批量归一化层的统计信息非常有用,尤其是在使用 DistributedDataParallel (DDP) 时。
# 参数说明 :
# module ( nn.Module ) :需要转换的包含一个或多个 BatchNorm*D 层的模型或模块。
# process_group ( optional ) :进程组,用于限制同步的范围,默认为 None ,表示使用整个进程组(即全世界)。
# 返回值 :
# 返回原始的 module ,但其中所有的 BatchNorm*D 层都被转换为 SyncBatchNorm 层。如果原始 module 本身就是一个 BatchNorm*D 层,则返回一个新的 SyncBatchNorm 层对象。
# 工作方式 :
# convert_sync_batchnorm() 方法会递归遍历传入的 module ,查找所有的 BatchNorm*D 层,并将它们替换为 SyncBatchNorm 层。这样,当模型在分布式环境中训练时,不同进程中的 SyncBatchNorm 层可以同步统计信息,确保批量归一化层的均值和方差是跨所有进程一致的。
# 如果上述条件满足,将模型中的所有批量归一化层(BatchNorm)转换为同步批量归一化层(SyncBatchNorm)。 torch.nn.SyncBatchNorm.convert_sync_batchnorm(model) 会遍历模型中的所有层,并将找到的批量归一化层转换为同步批量归一化层。然后,使用 .to(device) 将模型移动到指定的设备(GPU)上。
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
# 记录一条日志信息,表示正在使用同步批量归一化。
LOGGER.info('Using SyncBatchNorm()') # 使用 SyncBatchNorm() 。
# 同步批量归一化是一种在多GPU训练中保持批量归一化层状态同步的技术。它确保了不同GPU上的批量归一化层使用相同的均值和方差,这对于保持模型性能的一致性非常重要。在分布式训练环境中, SyncBatchNorm 可以减少因批量归一化层状态不一致而导致的训练结果方差。通过 torch.nn.SyncBatchNorm.convert_sync_batchnorm 方法,可以方便地将模型中的批量归一化层转换为同步批量归一化层,以实现这一功能。
# 这段代码涉及到训练数据加载器(train loader)的创建和数据集标签的处理。
# Trainloader
# 调用 create_dataloader 函数来创建训练数据的加载器 train_loader 和数据集 dataset 。这个函数接收多个参数,包括 训练数据路径 train_path 。
# def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=None, augment=False, cache=False, pad=0.0, rect=False, rank=-1, workers=8, image_weights=False, close_mosaic=False, quad=False, min_items=0, prefix='', shuffle=False):
# -> 用于创建一个 PyTorch 数据加载器(DataLoader),这个加载器可以用于深度学习模型的训练或验证。返回创建的数据加载器实例。
# -> return loader(dataset, batch_size=batch_size, shuffle=shuffle and sampler is None, num_workers=nw, sampler=sampler, pin_memory=PIN_MEMORY,
# collate_fn=LoadImagesAndLabels.collate_fn4 if quad else LoadImagesAndLabels.collate_fn, worker_init_fn=seed_worker, generator=generator), dataset
train_loader, dataset = create_dataloader(train_path,
# 图像尺寸。
imgsz,
# 批量大小(考虑到多GPU环境,所以用 batch_size // WORLD_SIZE ) 。
batch_size // WORLD_SIZE,
# 网格尺寸。
gs,
# 是否为单类别。
single_cls,
# 超参数 。
hyp=hyp,
# 是否进行数据增强。
augment=True,
# 缓存设置 cache 。
cache=None if opt.cache == 'val' else opt.cache,
# 是否使用矩形训练。
rect=opt.rect,
# 当前进程的局部排名。
rank=LOCAL_RANK,
# 工作线程数。
workers=workers,
# 是否使用图像权重。
image_weights=opt.image_weights,
# 是否关闭马赛克数据增强。
close_mosaic=opt.close_mosaic != 0,
# 是否使用四倍数据增强。
quad=opt.quad,
# 前缀。
prefix=colorstr('train: '),
# 是否在每个epoch开始时打乱数据。
shuffle=True,
# 最小项目数。
min_items=opt.min_items)
# 使用 numpy.concatenate 函数将数据集中所有标签沿着第一个维度(通常是批次维度)连接起来,得到一个包含所有标签的数组。
labels = np.concatenate(dataset.labels, 0)
# 找到连接后的标签数组中第一列(通常代表类别标签)的最大值,这个值表示数据集中的最大类别索引。
mlc = int(labels[:, 0].max()) # max label class
# 使用 assert 语句来确保最大类别索引 mlc 小于模型配置中的类别数 nc 。如果 mlc 大于或等于 nc ,则会抛出一个异常,提示标签类别超出了模型支持的范围。
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' # 标签类 {mlc} 超出了 {data} 中的 nc={nc}。可能的类标签为 0-{nc - 1} 。
# 这段代码的目的是在训练开始前设置训练数据加载器,并确保数据集中的标签与模型配置一致。通过创建数据加载器,可以将数据有效地送入模型进行训练。同时,通过检查标签类别的范围,可以避免在训练过程中出现类别索引错误,确保模型能够正确地处理输入数据。
# 这段代码处理的是训练流程中的一些特定步骤,特别是在进程0(即主进程或单GPU训练环境)中执行的操作。
# Process 0
# 这个条件语句检查 RANK 变量的值。 -1 表示非分布式训练环境(单GPU训练), 0 表示分布式训练环境中的主进程。如果条件满足,执行以下代码块。
if RANK in {-1, 0}:
# 调用 create_dataloader 函数来创建验证数据的加载器 val_loader 。这个函数接收多个参数,包括 验证数据路径 val_path 。 函数返回一个包含数据加载器和数据集的元组,这里只取第一个元素,即 val_loader 。
val_loader = create_dataloader(val_path,
# 图像尺寸。
imgsz,
# 批量大小(考虑到多GPU环境,所以用 batch_size // WORLD_SIZE * 2 ,这里乘以2可能是因为验证时不需要梯度,可以增加批量大小以加快验证速度)。
batch_size // WORLD_SIZE * 2,
# 网格尺寸。
gs,
# 是否为单类别。
single_cls,
# 超参数。
hyp=hyp,
# 缓存设置 。
cache=None if noval else opt.cache,
# 是否使用矩形训练。
rect=True,
# 当前进程的局部排名。
rank=-1,
# 工作线程数 workers * 2 (验证时使用的工作线程数是训练时的两倍)。
workers=workers * 2,
# 填充比例。
pad=0.5,
# 前缀。
prefix=colorstr('val: '))[0]
# 条件语句检查是否从之前的训练状态恢复。如果不是恢复训练,执行以下代码块。
if not resume:
# if not opt.noautoanchor:
# check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor
# 如果不是恢复训练,将模型的参数转换为半精度浮点数( half ),然后再转换为单精度浮点数( float )。这可能是为了减少模型参数的精度,以测试或调整锚点(anchors)。
model.half().float() # pre-reduce anchor precision
# 调用 callbacks 对象的 run 方法,并传入 'on_pretrain_routine_end' 事件以及标签 labels 和类别名称 names 。这通常用于在预训练流程结束时执行一些操作,如记录日志、保存模型状态等。
callbacks.run('on_pretrain_routine_end', labels, names)
# 这段代码的目的是在训练流程开始之前,在主进程中设置验证数据加载器,并在不是从检查点恢复训练的情况下,调整模型参数的精度。此外,它还提供了一个回调机制,用于在预训练流程结束时执行自定义操作。这些步骤有助于确保训练流程的顺利进行,并允许用户在关键点插入自定义逻辑。
# 这段代码处理的是在使用 CUDA(即在 GPU 上运行)且不在单 GPU 环境( RANK != -1 )时的分布式数据并行(Distributed Data Parallel,简称 DDP)模式的设置。
# DDP mode
# 这个条件语句检查两个条件 : cuda 是否使用 CUDA,即是否在 GPU 上运行。 RANK != -1 表示当前进程不是单 GPU 环境,而是分布式训练环境中的某个进程( RANK 是分布式训练中每个进程的唯一标识)。
if cuda and RANK != -1:
# 如果上述条件满足,即在分布式训练环境中且使用 GPU,调用 smart_DDP 函数将模型包装成分布式数据并行的形式。 smart_DDP函数是一个自定义函数,它返回一个使用 torch.nn.parallel.DistributedDataParallel 包装后的模型。这个包装后的模型能够在多个 GPU 上并行训练,同时保持每个进程中模型的状态同步。
# def smart_DDP(model):
# -> 用于创建一个分布式数据并行(Distributed Data Parallel,简称 DDP)模型,同时包含了对 PyTorch 版本的检查。
# -> return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK, static_graph=True) / return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
model = smart_DDP(model)
# torch.nn.parallel.DistributedDataParallel 是 PyTorch 提供的一个模块,用于在分布式训练环境中实现模型的自动并行化。它将数据自动分割到多个 GPU 上,并在每个 GPU 上并行执行模型的前向传播和反向传播,然后将结果收集并返回。在反向传播时,它也会自动将梯度收集并求和,实现跨多个 GPU 的梯度同步。
# 这段代码的目的是在使用 GPU 且在分布式训练环境中时,将模型转换为分布式数据并行的形式,以加速训练过程并提高计算效率。通过使用 DDP,可以利用多个 GPU 进行训练,同时保持模型状态的同步,这对于大规模训练任务尤其重要。
# 这段代码涉及到为模型设置属性,这些属性通常与模型的训练和性能评估有关。
# Model attributes
# 获取模型中检测层的数量 nl 。 de_parallel(model) 是一个函数,用于移除模型的并行包装(如 DataParallel 或 DistributedDataParallel ),以便访问模型的实际层。 model[-1] 访问模型的最后一个层,通常是输出层, .nl 获取该层的属性,这里假设 .nl 表示检测层的数量。
# def de_parallel(model): -> 将一个可能处于并行状态(例如使用 PyTorch 的 DataParallel 或 DistributedDataParallel 包装过的模型)转换回单个 GPU 或 CPU 上的模型。 -> return model.module if is_parallel(model) else model
nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps)
# 这是一个被注释掉的代码行,如果取消注释,它会根据检测层的数量 nl 调整超参数 hyp['box'] 的值。
#hyp['box'] *= 3 / nl # scale to layers
# 这也是一个被注释掉的代码行,如果取消注释,它会根据类别数 nc 和检测层的数量 nl 调整超参数 hyp['cls'] 的值。
#hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers
# 这同样是一个被注释掉的代码行,如果取消注释,它会根据图像尺寸 imgsz 、图像尺寸的基准值(640)和检测层的数量 nl 调整超参数 hyp['obj'] 的值。
#hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers
# 设置超参数中的 label_smoothing 值,根据 opt.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 函数根据数据集中的标签计算每个类别的权重,通常用于处理类别不平衡问题。计算出的权重被乘以类别数 nc ,然后移动到指定的设备(如 GPU)上。
# def labels_to_class_weights(labels, nc=80): -> 根据训练标签计算每个 类别 的权重,这通常用于在目标检测任务中平衡类别不均衡的问题。 转换为 PyTorch 张量。将 NumPy 数组转换为 PyTorch 张量,并确保数据类型为浮点数。 -> return torch.from_numpy(weights).float()
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights
# 将类别名称列表 names 附加到模型的属性上,这样模型就可以在推理时返回类别的名称。
model.names = names
# 这段代码的目的是为模型设置一些关键属性,包括检测层的数量、超参数、类别数量、类别权重和类别名称。这些属性对于模型的训练和性能评估至关重要,它们帮助模型更好地适应数据集的特点,如类别不平衡,并在推理时提供更多的上下文信息。
# 这段代码标志着训练过程的开始,包括初始化一些训练过程中需要的变量和设置,并进入主训练循环。
# Start training
# 记录训练开始的时间戳。
t0 = time.time()
# 获取训练数据加载器 train_loader 中的批次总数。
nb = len(train_loader) # number of batches
# 计算热身(warmup)迭代次数,即在训练开始时逐渐增加学习率的迭代次数。这个值是超参数 hyp['warmup_epochs'] 与批次总数 nb 的乘积,并且至少为100次迭代。
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations)
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
# 初始化上一次优化步骤的索引。
last_opt_step = -1
# 初始化一个数组来存储每个类别的平均精度(mAP)。
maps = np.zeros(nc) # mAP per class
# 初始化一个元组来存储训练结果,包括 精确度(P) 、 召回率(R) 、 不同阈值下的平均精度(mAP@.5, mAP@.5-.95) 和 验证损失(box, obj, cls) 。
results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls)
# 设置学习率调度器的当前周期,确保从 start_epoch 开始训练。
scheduler.last_epoch = start_epoch - 1 # do not move
# 初始化梯度缩放器,用于混合精度训练,以防止梯度下溢。
scaler = torch.cuda.amp.GradScaler(enabled=amp)
# 初始化早停机制,用于在训练过程中如果连续多个周期没有改进则停止训练。
# class EarlyStopping:
# -> 用于实现早停(early stopping)机制,这是一种在训练机器学习模型时用来防止过拟合的技术。
# -> def __init__(self, patience=30):
stopper, stop = EarlyStopping(patience=opt.patience), False
# 初始化损失计算类。
# class ComputeLoss:
# -> 这个类整合了多种损失函数,包括类别损失、边界框损失(IoU损失和DFLoss),并且支持标签平滑和焦点损失。
# -> def __init__(self, model, use_dfl=True):
compute_loss = ComputeLoss(model) # init loss class
# 调用回调函数,通知训练开始。
callbacks.run('on_train_start')
# 记录训练的基本信息,包括 图像尺寸 、 数据加载工作线程数 、 结果日志保存位置 和 训练周期数 。
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...') # 图像大小 {imgsz} train, {imgsz} val 使用 {train_loader.num_workers * WORLD_SIZE} 数据加载器工作者将结果记录到 {colorstr('bold', save_dir)} 开始训练 {epochs} 个时期...
# 进入主训练循环,从 start_epoch 开始,直到 epochs 结束。
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
# 在每个训练周期开始时调用回调函数。
callbacks.run('on_train_epoch_start')
# 将模型设置为训练模式。
model.train()
# 这段代码初始化了训练所需的各种变量和设置,包括时间记录、批次数、热身迭代次数、优化步骤索引、mAP存储数组、结果元组、学习率调度器、早停机制、梯度缩放器、损失计算类和回调函数。然后,它进入主训练循环,开始执行训练过程。这些步骤为模型的训练提供了必要的基础设施和监控,确保训练过程可以顺利进行,并在需要时提供适当的反馈和调整。
# 这段代码涉及到在训练过程中的一些可选设置,特别是针对单GPU训练环境。
# Update image weights (optional, single-GPU only)
# 条件语句检查是否启用了图像权重( image_weights ),这是一个用户选项,用于根据类别权重调整数据集中每个图像的采样概率。
if opt.image_weights:
# 如果启用了图像权重,计算类别权重。 model.class_weights 是模型属性中存储的类别权重,转换为CPU张量并转换为NumPy数组。然后,这些权重被调整为 (1 - maps) ** 2 / nc ,其中 maps 是每个类别的平均精度, nc 是类别总数。这种调整旨在给予表现不佳的类别更大的权重。
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights
# 根据类别权重计算图像权重。 labels_to_image_weights 函数根据数据集中的标签和类别权重来计算每个图像的权重。
# def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)): -> 根据训练标签计算每个 图像 的权重。返回值。函数返回一个数组,其中每个元素代表对应图像的权重。 -> return (class_weights.reshape(1, nc) * class_counts).sum(1)
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights
# 使用图像权重来随机选择数据集中的图像索引。 random.choices 函数根据提供的权重 iw 来选择图像, k=dataset.n 表示选择的数据量与数据集大小相同。
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx
# 条件语句检查是否到达了关闭马赛克(mosaic)数据增强的周期。 opt.close_mosaic 是用户指定的周期数,用于决定何时停止使用马赛克数据增强。
if epoch == (epochs - opt.close_mosaic):
# 如果到达了指定周期,记录一条日志信息,表示关闭了马赛克数据增强。
LOGGER.info("Closing dataloader mosaic") # 关闭数据加载器马赛克。
# 将数据集的 mosaic 属性设置为 False ,以禁用马赛克数据增强。
dataset.mosaic = False
# 这是一个注释掉的代码块,用于更新马赛克数据增强的边界。如果需要使用这个功能,可以取消注释。
# Update mosaic border (optional)
# 计算马赛克数据增强的边界值 b ,它是一个介于图像尺寸的25%到75%之间的随机值,并且是网格尺寸 gs 的倍数。
# b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs)
# 设置马赛克数据增强的边界, b - imgsz 用于高度边界, -b 用于宽度边界。
# dataset.mosaic_border = [b - imgsz, -b] # height, width borders
# 这段代码提供了在训练过程中根据类别表现调整图像采样概率的功能,以及控制马赛克数据增强的开关和边界。这些设置有助于提高模型在处理类别不平衡数据集时的性能,并允许用户根据需要调整数据增强策略。
# 这段代码是训练循环的一部分,涉及到初始化损失跟踪、设置训练数据加载器、日志记录以及数据的预处理。
# 初始化一个包含3个元素的张量 mloss ,用于存储和跟踪三种损失(例如,框损失、分类损失和置信度损失)的平均值。这个张量被放置在指定的设备上(如GPU)。
mloss = torch.zeros(3, device=device) # mean losses
# 条件语句检查是否处于分布式训练环境中。如果 RANK 不为 -1 ,则表示这是一个分布式训练环境。
if RANK != -1:
# 如果处于分布式训练环境,设置训练数据加载器的采样器以确保数据在每个epoch中正确地被打乱。
train_loader.sampler.set_epoch(epoch)
# enumerate(iterable, start=0)
# enumerate() 是 Python 内置的一个函数,它用于将一个可迭代对象(如列表、元组、字符串等)组合为一个索引序列,同时列出数据和数据下标。这在 Python 编程中非常有用,尤其是当你需要在循环中同时获取元素和它们的索引时。
# 参数 :
# iterable : 一个可迭代对象,比如列表、元组、字符串等。
# start=0 : 一个可选的整数参数,用于指定索引的起始值,默认为 0。
# 返回值 :
# 返回一个枚举对象,该对象生成包含索引和值的元组。
# enumerate() 函数在循环中提供了一种简洁的方式来跟踪当前元素的索引,这使得代码更加清晰和易于维护。
# 创建一个枚举器 pbar ,用于迭代训练数据加载器 train_loader 中的批次。
pbar = enumerate(train_loader)
# 使用日志记录器 LOGGER 记录训练过程中将要跟踪的信息,包括 Epoch 、 GPU内存使用 、 框损失 、 分类损失 、 置信度损失 、 实例数 和 图像尺寸 。
LOGGER.info(('\n' + '%11s' * 7) % ('Epoch', 'GPU_mem', 'box_loss', 'cls_loss', 'dfl_loss', 'Instances', 'Size'))
# 条件语句检查是否处于单GPU训练环境或非分布式训练环境。
if RANK in {-1, 0}:
# 如果处于单GPU训练环境或非分布式训练环境,使用 tqdm 库包装 pbar 以创建一个进度条,显示训练进度。
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar
# 在每个批次开始前,清除优化器中的梯度信息,为新的批次准备。
optimizer.zero_grad()
# 进入批次循环,迭代训练数据加载器中的每个批次。每个批次包含 图像 imgs 、 目标 targets 、 路径 paths 和 其他信息 _ 。
for i, (imgs, targets, paths, _) in pbar: # batch -------------------------------------------------------------
# 在每个批次开始时调用回调函数,通知批次训练开始。
callbacks.run('on_train_batch_start')
# 计算自训练开始以来的 累积批次数 ni 。
ni = i + nb * epoch # number integrated batches (since train start)
# 将图像数据移动到指定的设备上(如GPU),转换为浮点数,并归一化到0.0到1.0的范围内。
imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0
# 这段代码准备了训练过程中的损失跟踪、数据加载和日志记录,并处理了数据的预处理。它还确保了在每个批次开始前清除梯度,并在每个批次开始时调用回调函数。这些步骤为模型的批次训练提供了必要的基础设施和监控,确保训练过程可以顺利进行,并在需要时提供适当的反馈和调整。
# 这段代码处理训练过程中的热身(warmup)阶段,其中学习率和其他超参数会从初始值平滑地增加到预定值。
# Warmup
# 条件语句检查当前累积批次数 ni 是否小于或等于热身迭代次数 nw 。如果是,那么执行热身逻辑。
if ni <= nw:
# 初始化一个列表 xi ,包含热身阶段的起始和结束迭代次数,用于插值计算。
xi = [0, nw] # x interp
# np.interp(x, xp, fp, left=None, right=None, period=None)
# np.interp 是 NumPy 库中的一个函数,用于一维线性插值。给定一组数据点 x 和相应的值 xp ,以及一个新的查询点 x , np.interp 函数会找到 xp 中 x 值所在的区间,并使用线性插值来估计 x 对应的值。
# 参数说明 :
# x :查询点,即你想要插值的点。
# xp :数据点,一个一维数组,包含数据点的横坐标。
# fp : xp 对应的值,一个一维数组,包含数据点的纵坐标。
# left :可选参数,如果 x 中的值小于 xp 中的最小值,则使用这个值作为插值结果。
# right :可选参数,如果 x 中的值大于 xp 中的最大值,则使用这个值作为插值结果。
# period :可选参数,表示周期性,如果指定, xp 将被视为周期性的。
# 返回值 :
# 插值结果,一个与 x 形状相同的数组。
# compute_loss.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou)
# 使用 np.interp 函数计算当前批次 ni 的累积梯度次数。这个值根据热身阶段的迭代次数 nw 和名义批量大小 nbs 相对于实际批量大小 batch_size 的比例进行插值。 round() 函数用于将结果四舍五入到最近的整数, max(1, ...) 确保累积次数至少为1。
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
# 对于每个参数组,使用 np.interp 函数根据当前批次 ni 在热身阶段的位置插值计算学习率。如果是第一个参数组(通常用于偏置),学习率从 hyp['warmup_bias_lr'] 增加到 x['initial_lr'] * lf(epoch) ;否则,从0.0增加到 x['initial_lr'] * lf(epoch) 。
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:
# 使用 np.interp 函数根据当前批次 ni 在热身阶段的位置插值计算动量值,从 hyp['warmup_momentum'] 增加到 hyp['momentum'] 。
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])
# 这段代码的目的是在训练的热身阶段平滑地调整学习率和动量,以避免训练初期的不稳定。通过插值计算,这些超参数从热身阶段的初始值逐渐增加到正式训练的预定值。这种方法有助于模型在训练初期更稳定地收敛,尤其是在使用较大学习率时。
# 这段代码处理在训练过程中的多尺度(multi-scale)训练,这是一种数据增强技术,用于提高模型对不同尺寸输入的泛化能力。
# Multi-scale
# 条件语句检查是否启用了多尺度训练,这是一个用户选项。
if opt.multi_scale:
# 如果启用了多尺度训练,随机生成一个图像尺寸 sz ,范围在 imgsz 的50%到150%之间,并且是网格尺寸 gs 的倍数。 random.randrange 函数生成一个随机数, // gs * gs 确保尺寸是 gs 的整数倍。
sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs # size
# 计算缩放因子 sf ,即新尺寸 sz 与图像最大维度(高度或宽度)的比值。
sf = sz / max(imgs.shape[2:]) # scale factor
# 如果缩放因子 sf 不等于1,即新尺寸与原始尺寸不同,执行以下操作。
if sf != 1:
# 计算新的图像尺寸 ns ,确保新尺寸是 gs 的倍数。这里使用 math.ceil 函数向上取整,然后乘以 gs 。
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple)
# torch.nn.functional.interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None, antialiasing=False)
# 在PyTorch中, F.interpolate 函数是 torch.nn.functional.interpolate 的别名,它用于对图像或特征图进行上采样(放大)或下采样(缩小)。这个函数非常灵活,支持多种插值方法,可以用于深度学习模型中的特征图尺寸调整。
# 参数解释 :
# input :要进行插值的输入张量,通常是4维的,形状为 (batch_size, channels, height, width) 。
# size :目标输出尺寸,可以是整数或者元组。如果为 None ,则使用 scale_factor 来计算输出尺寸。
# scale_factor :缩放因子,可以是浮点数或者元组。如果为 None ,则使用 size 参数。
# mode :插值模式,常用的有 :
# 'nearest' :最近邻插值。
# 'linear' :线性插值(仅适用于1维数据)。
# 'bilinear' :双线性插值(适用于2维数据,如图像)。
# 'bicubic' :双三次插值(适用于2维数据,比双线性更平滑)。
# 'trilinear' :三线性插值(适用于3维数据)。
# align_corners :在某些插值模式下,这个参数控制角落对齐的行为。如果设置为 None ,则对于不同的插值模式有不同的默认行为。
# recompute_scale_factor :这个参数用于重新计算缩放因子,通常在 align_corners=None 时使用。
# antialiasing :在下采样时是否应用反锯齿,以减少混叠效应。
# 使用双线性插值(bilinear interpolation)对图像进行插值,以适应新的尺寸 ns 。 align_corners=False 参数用于控制插值时的对齐方式。
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False)
# 这段代码的目的是在训练过程中引入多尺度训练,通过随机改变图像的尺寸来增强模型的泛化能力。这种方法可以帮助模型更好地处理不同尺寸的输入,提高模型对尺度变化的鲁棒性。通过随机缩放图像,模型可以学习到更多关于图像内容的信息,而不仅仅局限于特定尺寸的图像。
# 这段代码描述了在训练循环中的前向传播和反向传播步骤,包括使用自动混合精度(AMP)和计算损失。
# Forward
# 前向传播。
# 这个上下文管理器启用自动混合精度(AMP),它允许模型在训练时使用16位浮点数(FP16)来加速计算并减少内存使用,同时保持32位浮点数(FP32)的动态范围以避免数值问题。
with torch.cuda.amp.autocast(amp):
# 执行模型的前向传播,将处理过的图像 imgs 通过模型获取预测结果 pred 。
pred = model(imgs) # forward
# 计算模型预测 pred 和目标 targets 之间的损失。 compute_loss 函数返回 总损失 loss 和 损失项 loss_items 。这里 loss 已经被批次大小 batch_size 缩放,以便于后续的梯度计算。
loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size
# 条件语句检查是否处于分布式训练环境中。如果 RANK 不为 -1 ,则表示这是一个分布式训练环境。
if RANK != -1:
# 如果处于分布式训练环境,将损失 loss 乘以 WORLD_SIZE (即参与训练的设备总数),以便在反向传播时平均梯度。
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode
# 条件语句检查是否启用了四倍损失(quadruple loss),这是一个用户选项。
if opt.quad:
# 如果启用了四倍损失,将损失 loss 乘以4,以加强对损失的惩罚。
loss *= 4.
# Backward
# 反向传播。
# 使用 scaler (一个 GradScaler 实例)来缩放损失 loss ,然后执行反向传播。 GradScaler 用于在AMP环境中管理梯度的缩放,以防止梯度下溢。
scaler.scale(loss).backward()
# 这段代码在训练循环中实现了前向传播和反向传播的关键步骤。通过使用自动混合精度和梯度缩放,它提高了训练的效率和稳定性。在前向传播中,模型生成预测,然后计算损失。在反向传播中,根据损失计算梯度,并使用 GradScaler 进行缩放以防止数值问题。这些步骤对于模型的学习过程至关重要,确保了模型参数可以根据损失函数有效地更新。
# 这段代码处理训练过程中的优化步骤,包括梯度的缩放、裁剪、应用优化器步骤以及更新指数移动平均(EMA)。
# Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
# 条件语句检查自上次优化步骤以来 是否累积了足够的批次 。 ni 是当前的累积批次数, last_opt_step 是上次优化步骤的批次数, accumulate 是需要累积的批次数。如果条件满足,执行优化步骤。
if ni - last_opt_step >= accumulate:
# 使用 GradScaler 的 unscale_ 方法来撤销梯度的缩放。这是在AMP环境中必要的步骤,用于恢复梯度的真实值,以便正确地更新模型参数。
scaler.unscale_(optimizer) # unscale gradients
# torch.nn.utils.clip_grad_norm_(parameters, max_norm, norm_type=2)
# torch.nn.utils.clip_grad_norm_() 是 PyTorch 中的一个函数,用于裁剪(限制)模型参数梯度的范数,以防止梯度爆炸问题。这个函数会缩放梯度,使得它们的最大范数不超过指定的阈值。
# 参数 :
# parameters :PyTorch 参数(或参数组)的迭代器,通常是模型的参数。
# max_norm :梯度的最大范数。如果计算出的范数超过这个值,梯度将被缩放。
# norm_type :(可选)用于计算梯度范数的类型,默认为2,表示L2范数。也可以使用其他值,如1(L1范数)或 np.inf (无穷范数,即最大值)。
# 返回值 :无返回值。该函数直接修改输入的参数梯度。
# 使用场景 :
# 在训练神经网络时,特别是在使用RNN或其他容易产生梯度爆炸的结构时,使用梯度裁剪来保持训练的稳定性。
# 使用 PyTorch 的 clip_grad_norm_ 函数来裁剪模型参数的梯度,以防止梯度爆炸。 max_norm=10.0 指定了梯度的最大范数,超过这个值的梯度将被缩放。
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) # clip gradients
# 使用 GradScaler 的 step 方法来执行优化器的 step 方法,更新模型参数。在AMP环境中,这一步是在梯度缩放之后执行的,以确保参数更新的稳定性。
scaler.step(optimizer) # optimizer.step
# 更新 GradScaler ,为下一次反向传播准备。
scaler.update()
# 清除优化器的梯度信息,为下一个批次的训练准备。
optimizer.zero_grad()
# 条件语句检查是否初始化了EMA(指数移动平均)。
if ema:
# 如果初始化了EMA,使用EMA更新模型参数。EMA是一种技术,通过计算模型参数的指数移动平均来平滑参数变化,有助于提高模型的泛化能力。
ema.update(model)
# 更新 last_opt_step 为当前批次数 ni ,标记最后一次优化步骤的位置。
last_opt_step = ni
# 这段代码在训练循环中实现了优化步骤,包括梯度的缩放撤销、裁剪、参数更新以及EMA更新。这些步骤确保了模型参数可以有效地根据损失函数更新,同时防止了梯度爆炸和梯度下溢的问题。EMA的使用进一步提高了模型的稳定性和泛化能力。
# 这段代码处理训练过程中的日志记录和进度更新,特别是在单GPU训练环境或非分布式训练环境的主进程中。
# Log
# 条件语句检查是否处于单GPU训练环境( RANK = -1 )或分布式训练环境的主进程( RANK = 0 )。
if RANK in {-1, 0}:
# 更新平均损失 mloss 。这里 i 是当前批次的索引, loss_items 是当前批次的损失项。这个公式是计算动态平均值的一种方式。
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses
# 计算并格式化当前GPU的内存使用情况。如果CUDA可用,计算保留的内存量(以GB为单位),否则返回0。
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB)
# pbar.set_description(description)
# pbar.set_description() 是 Python 中 tqdm 库的一个方法,用于设置进度条的描述信息。 tqdm 是一个快速、扩展性强的进度条工具库,可以在 Python 长循环中添加一个进度提示信息。
# 参数 :
# description :一个字符串,用来描述进度条当前的状态或者正在进行的任务。
# 特点和注意事项 :
# set_description() 方法可以在循环过程中动态更新进度条的描述信息,这对于显示当前任务的状态非常有用。
# 描述信息会显示在进度条的左侧。
# 如果你想要在进度条的右侧显示额外的信息,可以使用 set_postfix() 方法。
# tqdm 会自动处理描述信息的更新,不需要手动刷新屏幕。
# tqdm 是一个非常灵活的工具,可以通过各种参数和方法来自定义进度条的外观和行为, set_description() 只是其中的一个功能。
# 使用 tqdm 进度条的 set_description 方法更新进度条的描述,显示 当前epoch 、 GPU内存使用 、 平均损失项 、 目标数量 和 图像尺寸 。
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))
# 检查回调函数是否设置了停止训练的标志。
if callbacks.stop_training:
# 如果需要停止训练,提前退出训练循环。
return
# 这段代码的目的是在训练过程中记录和显示重要的信息,包括平均损失、GPU内存使用和进度条的更新。它还提供了一个机制来响应回调函数的停止训练请求。这些步骤有助于监控训练过程,并在必要时优雅地中断训练。通过这种方式,用户可以实时了解训练进度,并根据需要控制训练流程。
# end batch ------------------------------------------------------------------------------------------------
# 这段代码处理训练过程中的学习率调度器(scheduler)步骤以及在每个训练周期结束时的验证(validation)和平均精度(mAP)计算。
# Scheduler
# 从优化器的参数组中提取当前的学习率,并将其存储在列表 lr 中,以便后续记录日志时使用。
lr = [x['lr'] for x in optimizer.param_groups] # for loggers
# 调用学习率调度器的 step 方法,根据预设的策略更新学习率。
scheduler.step()
# 训练周期结束时的操作。
# 条件语句检查是否处于单GPU训练环境或分布式训练环境的主进程。
if RANK in {-1, 0}:
# mAP
# 在每个训练周期结束时调用回调函数,通知周期结束,并传递当前周期 epoch 。
callbacks.run('on_train_epoch_end', epoch=epoch)
# 如果使用了指数移动平均(EMA),这行代码更新EMA模型的属性,以确保EMA模型与主模型保持同步。
ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights'])
# 计算一个布尔值 final_epoch ,指示是否是最后一个训练周期或早停机制可能触发的周期。
final_epoch = (epoch + 1 == epochs) or stopper.possible_stop
# 条件语句检查是否跳过验证( noval )或是否是最后一个周期。如果不是跳过验证或处于最后一个周期,执行mAP计算。
if not noval or final_epoch: # Calculate mAP
# 验证和mAP计算。
# 调用 validate.run 函数执行验证步骤,计算模型在验证集上的性能。
# 这个函数接收多个参数,包括 数据字典 data_dict 。
# 函数返回 验证结果 results 、 每个类别的mAP maps 和 其他信息 。
results, maps, _ = validate.run(data_dict,
# 批量大小。
batch_size=batch_size // WORLD_SIZE * 2,
# 图像尺寸。
imgsz=imgsz,
# 是否使用半精度。
half=amp,
# 模型(这里使用EMA模型)。
model=ema.ema,
# 是否为单类别。
single_cls=single_cls,
# 数据加载器。
dataloader=val_loader,
# 保存目录。
save_dir=save_dir,
# 是否绘制图表。
plots=False,
# 回调函数。
callbacks=callbacks,
# 损失计算类。
compute_loss=compute_loss)
# 这段代码在每个训练周期结束时更新学习率、记录日志、同步EMA模型属性,并在适当的时候执行验证和mAP计算。这些步骤对于监控训练进度、调整学习率以及评估模型性能至关重要。通过在每个周期结束时计算mAP,可以及时发现模型性能的变化,从而做出相应的调整。
# 这段代码处理在每个训练周期结束后更新最佳平均精度(mAP)和执行早停检查的逻辑。
# Update best mAP
# 计算一个适应度分数 fi ,它是一个加权组合,通常包括精确度(P)、召回率(R)、在0.5 IoU阈值下的平均精度(mAP@.5)以及在0.5到0.95 IoU阈值下的平均精度(mAP@.5-.95)。 fitness 函数将这些指标组合成一个单一的分数,用于评估模型性能。
# def fitness(x): -> 计算模型的适应度,作为一系列度量指标的加权组合。返回值。函数返回一个包含每个模型适应度得分的数组。 -> return (x[:, :4] * w).sum(1)
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95]
# 调用 stopper 对象的函数,传入当前周期 epoch 和适应度分数 fi ,以检查是否满足早停条件。早停机制用于在模型性能不再提升时停止训练,以避免过拟合。
stop = stopper(epoch=epoch, fitness=fi) # early stop check
# 条件语句检查当前周期的适应度分数 fi 是否大于之前记录的最佳适应度分数 best_fitness 。
if fi > best_fitness:
# 如果当前周期的适应度分数更高,更新 best_fitness 为当前分数。
best_fitness = fi
# 创建一个日志值列表 log_vals ,包括 平均损失 mloss 、 验证结果 results 和 当前学习率 lr 。
log_vals = list(mloss) + list(results) + lr
# 调用回调函数 on_fit_epoch_end ,传递 日志值列表 log_vals 、 当前周期 epoch 、 最佳适应度分数 best_fitness 和 当前适应度分数 fi 。
callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi)
# 这段代码的目的是在每个训练周期结束后评估模型性能,并根据性能更新最佳mAP。同时,它还执行早停检查,以决定是否需要提前结束训练。通过记录和传递关键的性能指标,代码允许用户监控训练进度,并在训练过程中做出相应的调整。这些步骤对于确保模型在训练过程中达到最佳性能和避免过拟合至关重要。
# 这段代码处理模型的保存逻辑,确保在适当的条件下保存模型的状态。
# Save model
# 条件语句检查是否需要保存模型。如果 nosave 为 False (即用户没有指定不保存模型),或者如果这是最后一个训练周期且不使用进化策略( evolve ),则执行保存操作。
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模型的深拷贝(转换为半精度)。
'ema': deepcopy(ema.ema).half(),
# EMA的更新次数。
'updates': ema.updates,
# 优化器的状态。
'optimizer': optimizer.state_dict(),
# 训练选项。
'opt': vars(opt),
# Git信息。
'git': GIT_INFO, # {remote, branch, commit} if a git repo
# 当前日期。
'date': datetime.now().isoformat()}
# Save last, best and delete
# 将检查点字典 ckpt 保存到文件 last ,通常用于存储最近一次训练的状态。
torch.save(ckpt, last)
# 条件语句检查当前周期的适应度分数 fi 是否等于最佳适应度 best_fitness 。
if best_fitness == fi:
# 如果当前周期的适应度分数是最佳,将检查点字典 ckpt 保存到文件 best ,通常用于存储最佳模型的状态。
torch.save(ckpt, best)
# 条件语句检查是否按照用户指定的周期 save_period 保存模型。
if opt.save_period > 0 and epoch % opt.save_period == 0:
# 如果满足保存周期条件,将检查点字典 ckpt 保存到一个以当前周期编号命名的文件中。
torch.save(ckpt, w / f'epoch{epoch}.pt')
# 删除 ckpt 字典,释放内存。
del ckpt
# 调用回调函数 on_model_save ,传递最后保存的文件路径 last 、当前周期 epoch 、是否是最后一个周期 final_epoch 、最佳适应度 best_fitness 和当前适应度分数 fi 。
callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi)
# 这段代码的目的是在训练过程中保存模型的状态,包括最后的状态和最佳状态。它还提供了一个机制来定期保存模型,以及在训练结束时保存最终状态。通过这种方式,用户可以在训练过程中和结束后恢复模型,继续训练或用于推理。回调函数的使用允许用户在保存模型时执行额外的操作,如记录日志或执行自定义的保存逻辑。
# 这段代码是用于分布式数据并行(Distributed Data Parallel,简称DDP)训练中的一个片段,用于在训练过程中控制是否停止训练。
# EarlyStopping
# 检查变量 RANK 是否不等于 -1 。在分布式训练中, RANK 通常用来标识当前进程在所有进程中的编号。如果 RANK 不等于 -1 ,说明正在进行分布式训练。
if RANK != -1: # if DDP training
# 创建一个列表 broadcast_list ,如果当前进程的 RANK 是 0 ,则列表中包含变量 stop 的值;否则,列表中包含 None 。 RANK 为 0 的进程通常是主进程,这里使用条件表达式来决定是否将 stop 变量广播给其他进程。
broadcast_list = [stop if RANK == 0 else None]
# torch.distributed.broadcast_object_list(object_list, src=0, group=None, device=None)
# torch.distributed.broadcast_object_list 是 PyTorch 分布式通信包中的一个函数,用于将一个对象列表从一个进程(源进程)广播到所有其他进程。
# 参数 :
# object_list :要广播的输入对象列表。每个对象都必须是可pickle的(即可以通过Python的 pickle 模块进行序列化和反序列化)。
# src :源进程的排名,默认为0。只有 src 进程上的对象会被广播,但每个进程都必须提供大小相等的列表。
# group :(可选)要处理的进程组。如果没有指定,则使用默认进程组。默认为 None 。
# device :(可选)如果指定了 device ,则将对象序列化并转换为张量,在广播之前将其移动到 device 上。默认为 None 。
# 返回值 :该函数没有返回值。如果进程是组的一部分, object_list 将包含来自 src 进程广播的对象。
# 功能描述 :
# torch.distributed.broadcast_object_list 将 object_list 中的可pickle对象广播给整个进程组。这与 broadcast 函数类似,但可以传入Python对象。注意, object_list 中的所有对象都必须是可pickle的才能进行广播。
# 注意事项 :
# 对于基于NCCL的进程组,对象的内部张量表示必须在通信发生之前移动到GPU设备上。在这种情况下,使用的设备由 torch.cuda.current_device() 给出,用户有责任通过 torch.cuda.set_device() 确保将其设置为每个进程都有单独的GPU。
# 请注意,此API与 all_gather() 略有不同,因为它不提供 async_op 句柄,因此将是阻塞调用。
# broadcast_object_list() 隐式使用 pickle 模块,已知这是不安全的。可以构造恶意的pickle数据,该数据将在unpickling期间执行任意代码。仅使用您信任的数据调用此函数。
# 使用 dist 模块的 broadcast_object_list 函数将 broadcast_list 列表广播给所有进程。参数 0 指定了从 RANK 为 0 的进程开始广播。
dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks
# 检查当前进程的 RANK 是否不等于 0 ,即检查当前进程是否不是主进程。
if RANK != 0:
# 如果当前进程不是主进程,那么它会从 broadcast_list 列表中取出第一个元素,并将其赋值给 stop 变量。由于主进程广播了 stop 变量,所以这里实际上是接收主进程广播的 stop 值。
stop = broadcast_list[0]
# 检查 stop 变量的值,如果为 True ,则执行下一行代码。
if stop:
# 如果 stop 为 True ,则执行 break 语句,跳出循环。这意味着如果主进程决定停止训练,那么所有的进程都会跳出循环,从而停止训练。
break # must break all DDP ranks
# 这段代码的目的是实现在分布式训练中,由主进程控制是否停止训练。主进程通过广播 stop 变量的值给所有进程,如果 stop 为 True ,则所有进程都会停止训练。这样可以确保在分布式训练中,所有进程能够同步地停止训练。
# end epoch ----------------------------------------------------------------------------------------------------
# end training -----------------------------------------------------------------------------------------------------
# 这段代码是训练中的一个片段,用于在训练过程中记录信息、验证模型,并在训练结束时清理资源。
# 检查当前进程的 RANK 是否为 -1 或 0 。 RANK 为 -1 通常表示非分布式训练环境,而 RANK 为 0 表示分布式训练中的主进程。
if RANK in {-1, 0}:
# 如果当前进程是主进程或非分布式环境,这行代码记录当前完成的epoch数以及训练耗时(转换为小时)。
LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.') # {epoch - start_epoch + 1} 个纪元在 {(time.time() - t0) / 3600:.3f} 小时内完成。
# 遍历两个文件路径 : last 和 best 。 last 通常指的是最后保存的模型,而 best 指的是验证集上表现最好的模型。
for f in last, best:
# 检查文件 f 是否存在。
if f.exists():
# 检查当前遍历的文件是否是 last 文件。
if f is last:
# 如果当前文件是 last 文件,会移除模型中的优化器信息,以减少模型文件的大小。
# def strip_optimizer(f='best.pt', s=''): -> 从训练好的模型文件中移除优化器(optimizer)和其他一些不需要的键,以便在不继续训练的情况下使用模型进行推理。这通常用于模型的部署阶段,因为优化器在推理时是不需要的。
strip_optimizer(f, last_striped) # strip optimizers
# 如果当前文件不是 last 文件,执行else分支。
else:
# 如果当前文件是 best 文件,同样会移除模型中的优化器信息。
# def strip_optimizer(f='best.pt', s=''): -> 从训练好的模型文件中移除优化器(optimizer)和其他一些不需要的键,以便在不继续训练的情况下使用模型进行推理。这通常用于模型的部署阶段,因为优化器在推理时是不需要的。
strip_optimizer(f, best_striped) # strip optimizers
# 检查当前遍历的文件是否是 best 文件。
if f is best:
# 如果当前文件是 best 文件,记录开始验证该模型的信息。
LOGGER.info(f'\nValidating {f}...') # 正在验证 {f}...
# 调用 validate.run 函数来验证 best 模型的性能,并返回验证结果。
results, _, _ = validate.run(
# 一个字典,包含了验证过程中需要的数据信息。
data_dict,
# 设置了验证时的批量大小(batch size)。它首先将原始的批量大小 batch_size 除以全局的进程数 WORLD_SIZE (在分布式训练中,这通常是所有GPU的数量),然后乘以2。这样做的目的是为了在每个GPU上使用更大的批量大小,以便更有效地利用GPU资源。
batch_size=batch_size // WORLD_SIZE * 2,
# 设置输入图像的大小。
imgsz=imgsz,
# attempt_load(f, device) 尝试加载模型文件 f 到指定的设备 device 上。这里 f 是模型文件的路径。 .half() 将模型转换为半精度(float16),这有助于减少显存使用,并且在某些情况下可以加速计算。
model=attempt_load(f, device).half(),
# 指示是否是单类别分类任务。
single_cls=single_cls,
# val_loader 是用于验证的数据加载器,它负责从验证集提供数据。
dataloader=val_loader,
# 指定了保存验证结果的目录。
save_dir=save_dir,
# 如果是COCO数据集,指示是否保存验证结果为JSON格式。
save_json=is_coco,
# 设置为 True 表示在验证过程中打印详细信息。
verbose=True,
# 指示是否生成验证过程中的可视化图表。
plots=plots,
# callbacks 是一个回调函数列表,可以在验证过程中的特定时刻被调用。
callbacks=callbacks,
# compute_loss 参数指示是否在验证过程中计算损失。
compute_loss=compute_loss) # val best model with plots
# 检查是否是COCO数据集。
if is_coco:
# 如果是COCO数据集,会调用回调函数 on_fit_epoch_end ,并传入损失、验证结果、学习率等信息。
callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi)
# 在训练结束时调用回调函数 on_train_end ,并传入最后保存的模型、最佳模型、当前epoch和验证结果。
callbacks.run('on_train_end', last, best, epoch, results)
# 清理CUDA缓存,释放未被引用的GPU内存。
torch.cuda.empty_cache()
# 返回验证结果。
return results
# 这段代码的主要功能是在训练过程中,对于主进程或非分布式环境,记录训练进度和耗时,验证最后保存的模型和最佳模型的性能,并在训练结束时清理资源。对于最佳模型,还会进行额外的验证和回调函数调用。最后返回验证结果。
# 这个函数是一个典型的深度学习训练循环实现,涵盖了从数据加载、模型训练、验证到模型保存的全过程。它使用了 PyTorch 框架的许多高级特性,如自动混合精度训练(AMP)、分布式训练(DDP)和自定义回调函数。
3.def parse_opt(known=False):
# 这段代码定义了一个名为 parse_opt 的函数,它使用 argparse 库来解析命令行参数。这个函数通常用于配置训练模型时的各种选项。
# 定义一个函数 parse_opt ,它接受一个名为。
# 1.known :的布尔参数,默认值为 False 。
def parse_opt(known=False):
# argparse.ArgumentParser(prog=None, usage=None, description=None, epilog=None, parents=None, formatter_class=argparse.HelpFormatter, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='error', add_help=True)
# argparse.ArgumentParser() 是 Python 标准库 argparse 模块中的一个函数,用于创建命令行参数解析器。这个解析器能够从 sys.argv 中自动提取命令行参数,并根据定义的规则将它们转换为适当的类型,同时生成用户友好的帮助文档和使用说明。
# 参数解释 :
# prog : 指定程序的名称,默认为 sys.argv[0] 。
# usage : 指定使用说明的字符串,如果未提供,则自动生成。
# description : 指定在帮助文档中显示的描述性文本。
# epilog : 指定在帮助文档末尾显示的文本。
# parents : 一个 ArgumentParser 对象列表,用于合并多个解析器的参数。
# formatter_class : 指定用于生成帮助文档的格式化类,默认为 argparse.HelpFormatter 。
# prefix_chars : 指定参数名前缀字符,默认为 '-' 。
# fromfile_prefix_chars : 指定从文件中读取参数时的前缀字符。
# argument_default : 指定所有未明确指定默认值的命令行参数的默认值。
# conflict_handler : 指定如何处理参数之间的冲突,默认为 'error' 。
# add_help : 指定是否添加 --help 参数,默认为 True 。
# 返回值 :
# argparse.ArgumentParser() 函数返回一个 ArgumentParser 对象,该对象包含了一系列方法,用于添加和管理命令行参数,以及解析这些参数。
# 创建一个新的参数解析器对象。
parser = argparse.ArgumentParser()
# 在命令行接口中,长选项和短选项是两种不同的参数表示方式,它们允许用户指定选项或参数 :
# 长选项(Long Options) :
# 长选项通常以两个连字符 -- 开头,例如 --input 或 --verbose 。长选项一般用于表示更复杂的操作或名称较长的参数。它们更易于阅读和理解,因为它们可以包含完整的单词,而不需要缩写。
# 短选项(Short Options) :
# 短选项通常以单个连字符 - 开头,例如 -i 或 -v 。短选项是长选项的缩写形式,它们通常用于表示简单的操作或频繁使用的参数。短选项的一个限制是,它们只能包含单个字母,因此可能不如长选项那样直观。
# 特点和使用场景 :
# 可读性 :长选项由于可以包含完整的单词,因此通常更易于理解参数的意图。
# 简洁性 :短选项更短,因此输入起来更快,适合那些用户可能经常输入的常用参数。
# 组合 :短选项可以组合在一起,例如 -abc 可以表示 -a -b -c ,这在需要同时设置多个标志时非常方便。
# 互斥性 :长选项和短选项不应该同时表示同一个参数,以避免混淆。
# add_argument(name_or_flags..., action=None, nargs=None, const=None, default=None, type=None, choices=None, required=False, help=None, metavar=None, dest=None)
# add_argument() 是 argparse.ArgumentParser 类的一个方法,用于向解析器中添加一个新的命令行参数。这个方法非常灵活,允许你指定参数的各种属性,如名称、类型、默认值、帮助信息等。
# 参数解释 :
# name_or_flags : 一个或多个参数名称或标志。可以是 --name 、 -name 或仅 name 。如果以 - 开头,则为长选项;如果以单个 - 开头,则为短选项。
# action : 指定当参数被解析时应该执行的动作。常见的值有 store (存储值,默认行为)、 store_true 、 store_false 、 append 、 append_const 等。
# 以下是一些常用的 action 选项及其描述 :
# store (默认) :将命令行参数的值存储到目标位置。
# store_true :当命令行中出现此参数时,将其值设置为 True 。
# store_false :当命令行中出现此参数时,将其值设置为 False 。
# append :将命令行参数的值添加到列表中。如果之前没有值,会创建一个新的列表。
# append_const :将 const 参数指定的常量值添加到列表中。如果之前没有值,会创建一个新的列表。
# count :每次参数出现时,将目标值递增 1。
# help :显示帮助文档并退出程序。这是 --help 参数的默认行为。
# version :打印版本信息并退出程序。
# extend :将命令行参数的值添加到列表中,如果值已经存在,则将其扩展为包含所有元素的新列表。
# callback :调用一个函数,将命令行参数的值作为参数传递。
# callback_with_args :类似于 callback ,但允许额外的参数和关键字参数。
# nargs : 指定参数应该接收的值的数量。可以是整数或特殊字符串,如 '?' 、 '*' 、 '+' 。
# 以下是 nargs 参数的一些常见用法 :
# 整数 :指定参数应该接收确切的值的数量。
# '?' :参数是可选的,它可以接收零个或一个值。 如果命令行中未提供该参数,则使用 const 或 default 值(如果提供了这些值)。
# '' :参数可以接收零个或多个值,并将所有值收集到一个列表中。
# '+' :参数可以接收一个或多个值,并将所有值收集到一个列表中。
# ''** :剩余的所有参数都将被收集到一个列表中。 这通常用于将所有未解析的参数传递给一个函数。
# const : 与 action 参数一起使用,指定当使用 append_const 动作时的值。
# default : 指定参数的默认值。
# type : 指定用于转换命令行参数的类型。可以是 str 、 int 、 float 等,也可以是自定义函数。
# choices : 指定参数必须是其中之一的值的容器(如列表或元组)。
# required : 指定参数是否为必需的。
# help : 提供关于参数的帮助信息。
# metavar : 指定在帮助文档中显示的参数的替代名称。
# dest : 指定存储参数值的属性名,默认为参数名。
# add_argument() 方法是构建命令行接口时非常强大的工具,它允许开发者精确控制如何解析和处理用户输入。
# parser.add_argument('--weights', type=str, default=ROOT / 'yolo.pt', help='initial weights path')
# parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
# 添加一个命令行参数 --weights ,用于指定初始权重文件的路径,如果没有提供,则默认为空字符串。
parser.add_argument('--weights', type=str, default='', help='initial weights path')
# 添加一个命令行参数 --cfg ,用于指定模型配置文件的路径,默认为 yolo.yaml 。
parser.add_argument('--cfg', type=str, default='yolo.yaml', help='model.yaml path')
# 添加一个命令行参数 --data ,用于指定数据集配置文件的路径,默认为 ROOT 目录下的 data/coco128.yaml 。
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
# 添加一个命令行参数 --hyp ,用于指定超参数配置文件的路径,默认为 ROOT 目录下的 data/hyps/hyp.scratch-low.yaml 。
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path')
# 添加一个命令行参数 --epochs ,用于指定总的训练周期数,默认为100。
parser.add_argument('--epochs', type=int, default=100, help='total training epochs')
# 添加一个命令行参数 --batch-size ,用于指定所有GPU的总批量大小,默认为16,如果设置为-1,则使用自动批量大小。
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch')
# 添加一个命令行参数 --imgsz (或 --img 或 --img-size ),用于指定训练和验证时的图像大小,默认为640像素。
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 ,如果设置,则只保存最终的检查点。
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
# 添加一个命令行参数 --noval ,如果设置,则只在最后一个周期进行验证。
parser.add_argument('--noval', action='store_true', help='only validate final epoch')
# 添加一个命令行参数 --noautoanchor ,如果设置,则禁用自动锚点调整。
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor')
# 添加一个命令行参数 --noplots ,如果设置,则不保存任何绘图文件。
parser.add_argument('--noplots', action='store_true', help='save no plot files')
# 添加一个命令行参数 --evolve ,用于指定进化超参数的代数,默认为300。
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
# 添加一个命令行参数 --bucket ,用于指定gsutil桶的名称,默认为空字符串。
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
# 添加一个命令行参数 --cache ,用于指定图像缓存的位置,默认为 ram 。
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')
# 添加一个命令行参数 --device ,用于指定使用的设备,例如 0 或 0,1,2,3 表示GPU编号,或者 cpu 表示使用CPU。
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
# 添加一个命令行参数 --multi-scale ,如果设置,则在训练中变化图像大小±50%。
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
# 添加一个命令行参数 --single-cls ,如果设置,则将多类数据作为单类数据进行训练。
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
# 添加一个命令行参数 --optimizer ,用于指定优化器,默认为SGD,可选SGD、Adam、AdamW或LION。
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW', 'LION'], default='SGD', help='optimizer')
# 添加一个命令行参数 --sync-bn ,如果设置,则使用同步批量归一化,仅在分布式数据并行(DDP)模式下可用。
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
# 添加一个命令行参数 --workers ,用于指定最大数据加载器工作线程数,默认为8。
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
# 添加一个命令行参数 --project ,用于指定保存项目的路径,默认为 ROOT 目录下的 runs/train 。
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
# 添加一个命令行参数 --name ,用于指定保存实验的名称,默认为 exp 。
parser.add_argument('--name', default='exp', help='save to project/name')
# 添加一个命令行参数 --exist-ok ,如果设置,则允许项目/名称已存在,不进行递增。
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
# 添加一个命令行参数 --quad ,如果设置,则使用四倍数据加载器。
parser.add_argument('--quad', action='store_true', help='quad dataloader')
# 添加一个命令行参数 --cos-lr ,如果设置,则使用余弦学习率调度器。
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler')
# 添加一个命令行参数 --flat-cos-lr ,如果设置,则使用平坦余弦学习率调度器。
parser.add_argument('--flat-cos-lr', action='store_true', help='flat cosine LR scheduler')
# 添加一个命令行参数 --fixed-lr ,如果设置,则使用固定学习率调度器。
parser.add_argument('--fixed-lr', action='store_true', help='fixed LR scheduler')
# 添加一个命令行参数 --label-smoothing ,用于指定标签平滑的epsilon值,默认为0.0。
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
# 添加一个命令行参数 --patience ,用于指定早停耐心值,即在没有改进的情况下持续的周期数,默认为100。
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
# 添加一个命令行参数 --freeze ,用于指定冻结的层。 nargs='+' 表示这个参数需要一个或多个值, type=int 表示这些值应该是整数。默认值为 [0] ,意味着第一层(通常是输入层)被冻结。帮助信息提供了如何指定冻结层的例子。
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2')
# 添加一个命令行参数 --save-period ,用于指定每训练多少周期保存一次检查点。如果设置为小于1的值,则不保存检查点。
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
# 添加一个命令行参数 --seed ,用于设置全局训练的随机种子,默认为0。
parser.add_argument('--seed', type=int, default=0, help='Global training seed')
# 添加一个命令行参数 --local_rank ,用于分布式数据并行(DDP)中的自动多GPU设置,通常不需要手动修改。
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify')
# 添加一个命令行参数 --min-items ,用于实验性功能,具体作用未在帮助信息中说明,默认值为0。
parser.add_argument('--min-items', type=int, default=0, help='Experimental')
# 添加一个命令行参数 --close-mosaic ,同样用于实验性功能,具体作用未在帮助信息中说明,默认值为0。
parser.add_argument('--close-mosaic', type=int, default=0, help='Experimental')
# 这部分是与日志记录相关的参数。
# Logger arguments
# 添加一个命令行参数 --entity ,用于指定实体,具体含义未在帮助信息中说明,默认值为 None 。
parser.add_argument('--entity', default=None, help='Entity')
# 添加一个命令行参数 --upload_dataset ,用于指定是否上传数据集,如果提供了参数,则上传验证集;如果没有提供参数,则不上传。
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option')
# 添加一个命令行参数 --bbox_interval ,用于设置边界框图像日志记录的间隔,默认为-1,意味着不记录。
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval')
# 添加一个命令行参数 --artifact_alias ,用于指定使用的数据集版本,默认为 latest 。
parser.add_argument('--artifact_alias', type=str, default='latest', help='Version of dataset artifact to use')
# parse_args(args=None, namespace=None)
# parse_args() 是 argparse.ArgumentParser 类的一个方法,用于解析命令行参数。当创建了一个 ArgumentParser 对象并使用 add_argument() 方法添加了所有需要的参数后,会调用 parse_args() 方法来实际解析这些参数。
# 参数解释 :
# args : 一个字符串列表,用于替代 sys.argv[1:] (命令行参数列表)。如果为 None ,则默认使用 sys.argv[1:] 。
# namespace : 一个 Namespace 对象,用于存储解析后的参数值。如果为 None ,则 parse_args() 会创建一个新的 Namespace 对象。
# 返回值 :
# 返回一个 Namespace 对象,其中包含了所有解析后的命令行参数。
# 处理复杂情况 :
# 如果需要处理更复杂的情况,比如从文件中读取参数或者需要自定义解析逻辑, parse_args() 还提供了一些扩展机制,例如使用 parse_known_args() 方法来解析已知参数,或者通过继承 ArgumentParser 类并重写 parse_args() 方法来实现自定义解析逻辑。
# parse_args() 是 argparse 模块中的核心方法,它使得命令行参数的解析变得简单而强大。
# parse_known_args(args=None, namespace=None)
# parse_known_args() 是 argparse.ArgumentParser 类的一个方法,与 parse_args() 类似,它用于解析命令行参数,但它会忽略那些未知的参数,而不是抛出错误。这意味着如果命令行中包含了 ArgumentParser 对象中未定义的参数,这些参数将被静默忽略,而不是导致程序终止。
# 参数解释 :
# args : 一个字符串列表,用于替代 sys.argv[1:] (命令行参数列表)。如果为 None ,则默认使用 sys.argv[1:] 。
# namespace : 一个 Namespace 对象,用于存储解析后的参数值。如果为 None ,则 parse_known_args() 会创建一个新的 Namespace 对象。
# 返回值 :
# 返回一个元组,包含两个元素 :第一个是包含解析后的已知参数的 Namespace 对象,第二个是包含所有未解析参数的列表。
# 应用场景。parse_known_args() 方法特别适用于以下场景 :
# 当想要允许命令行接口接受一些程序不处理的参数时(例如,这些参数可能被传递给其他程序)。
# 当需要编写一个包装器或中间件,它需要处理一部分参数,而将其他参数传递给另一个命令行工具时。
# 通过使用 parse_known_args() ,可以更灵活地处理命令行参数,同时避免因为未知参数而导致程序错误退出。
# 这行代码根据 known 参数的值返回不同的结果。如果 known 为 True ,则返回解析后的已知参数;如果 known 为 False ,则返回解析后的所有参数。
return parser.parse_known_args()[0] if known else parser.parse_args()
# 这个 parse_opt 函数是一个参数解析器,它定义了一系列命令行参数,用于配置模型训练的不同方面,包括模型权重、配置文件、数据集、超参数、训练周期、批量大小、图像大小、训练模式等。这些参数可以通过命令行传入,也可以使用默认值。函数最后返回解析后的参数,以便在训练过程中使用。
4.def main(opt, callbacks=Callbacks()):
# 这段代码定义了一个名为 main 的函数,是一个深度学习训练程序的主函数,它处理参数检查、模型恢复、分布式训练设置、训练执行以及超参数演化(可选)。
# opt :通常是一个命名空间或者字典,包含了训练过程中所需的所有配置选项和参数。这些选项可能包括模型配置、数据路径、训练参数(如学习率、批次大小等)、验证和测试设置等。在深度学习训练中, opt 通常由命令行参数解析得到,或者从配置文件中加载。
# callbacks :这是一个可选参数,其默认值为 Callbacks() 类的一个实例。 callbacks 参数用于提供一组回调函数,这些函数可以在训练过程中的特定事件点被调用,例如在每个epoch结束时、在训练结束时或者在模型验证时。回调函数可以用来执行如日志记录、模型保存、学习率调整等操作。在这里, Callbacks() 是一个自定义类,定义了一些默认的回调行为,也可以被用户自定义扩展。
def main(opt, callbacks=Callbacks()):
# 这两行代码是 main 函数的一部分,用于在非分布式训练环境或分布式训练的主进程中打印训练参数。
# Checks
# 检查变量 RANK 的值。在分布式训练中, RANK 用于标识当前进程的编号。 -1 通常表示非分布式训练环境,而 0 表示分布式训练的主进程。这段代码确保只有在非分布式环境或主进程中才执行后续的操作。
if RANK in {-1, 0}:
# 调用 print_args 函数,传入 vars(opt) 作为参数。 vars(opt) 将 opt 对象(通常是一个命名空间或字典)转换为一个字典,其中包含了所有的属性和对应的值。 print_args 函数是用来打印这些参数,使得用户可以在训练开始前看到所有的配置选项。这有助于调试和确认训练设置是否正确。
# def print_args(args: Optional[dict] = None, show_file=True, show_func=False): -> 显示当前函数的参数,或者如果提供了参数字典,也可以显示任意函数的参数。
print_args(vars(opt))
# 这段代码的目的是确保在非分布式训练或分布式训练的主进程中打印出训练参数,以便用户可以验证配置是否正确。这是一个很好的实践,因为它可以帮助用户在训练开始前发现配置错误。
# 这段代码处理模型训练的恢复逻辑,包括从指定的或最新的检查点(checkpoint)恢复,以及设置训练参数。
# Resume (from specified or most recent last.pt)
# 条件语句检查是否需要恢复训练( opt.resume 为真),不是从Comet.ml恢复( check_comet_resume(opt) 为假),并且不是进行超参数演化( opt.evolve 为假)。
if opt.resume and not check_comet_resume(opt) and not opt.evolve:
# 如果 opt.resume 是字符串,使用 check_file 函数检查文件路径是否有效;否则,调用 get_latest_run 函数获取最新的运行记录。 Path 是Python的路径操作库,用于处理文件路径。
# def check_file(file, suffix=''): -> 检查一个文件是否存在,如果不存在且文件是一个网址,则下载该文件;如果文件是一个 ClearML 数据集 ID,则检查 ClearML 是否已安装;如果都不是,则在指定的目录中搜索文件。 -> return file / return files[0] # return file
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())
# 构造出训练选项的yaml文件路径,通常位于检查点文件的上两级目录。
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml
# 保存原始数据集的路径。
opt_data = opt.data # original dataset
# 检查 opt_yaml 路径的文件是否存在。
if opt_yaml.is_file():
# 如果文件存在,打开文件准备读取。
with open(opt_yaml, errors='ignore') as f:
# 使用 yaml.safe_load 函数加载yaml文件内容。
d = yaml.safe_load(f)
# 如果 opt_yaml 文件不存在,执行else分支。
else:
# torch.load(f, map_location=None, pickle_module=pickle, **pickle_load_args)
# torch.load 是 PyTorch 中用于加载保存的 PyTorch 对象的函数。这个函数可以加载之前使用 torch.save 保存的文件,这些文件可以包含模型参数、优化器状态、张量等。
# 参数说明 :
# f :要加载的文件名或文件对象。可以是一个字符串路径,也可以是一个文件对象。
# map_location :一个字符串或一个函数,指定如何映射存储位置。默认为 None ,意味着使用默认存储位置。如果设置为 'cpu' ,那么所有张量都会映射到CPU上。如果设置为 'cuda:device_id' ,那么张量会被映射到指定的GPU上。也可以是一个函数,该函数接受一个存储位置字符串并返回一个新的存储位置字符串。
# pickle_module :用于序列化和反序列化的模块。默认为 Python 标准库中的 pickle 。
# **pickle_load_args :传递给 pickle.load 函数的其他参数。
# 返回值 :
# 加载的 PyTorch 对象,这可以是张量、模型、优化器状态等。
# 使用PyTorch的 torch.load 函数加载检查点文件,并获取其中的 'opt' 键对应的值。
d = torch.load(last, map_location='cpu')['opt']
# ns = argparse.Namespace(**{'attr1': 'value1', 'attr2': 'value2'})
# argparse.Namespace 是 Python 标准库 argparse 模块中的一个类,它用于创建一个简单的命名空间对象,该对象将传入的关键字参数作为其属性存储。这种方式常用于存储命令行参数。
# argparse.Namespace 没有显式的构造函数定义,它通常通过关键字参数直接创建实例。这个类的实例类似于 Python 的普通对象,但是它的属性可以直接通过命令行参数的名称访问。
# 解释 :
# **d :这里的 d 是一个字典, ** 运算符用于将字典 d 解包为关键字参数。这意味着字典中的每个键值对都会被传递给 argparse.Namespace 构造函数,作为对象的属性。
# 特点 :
# argparse.Namespace 实例的属性可以直接通过点符号访问,就像访问任何其他对象的属性一样。
# 这个类通常与 argparse 模块一起使用,用于解析命令行参数。 argparse 解析器会自动将解析后的参数存储到一个 Namespace 对象中。
# 使用从检查点或yaml文件中获取的参数字典 d 创建一个新的 argparse.Namespace 对象,替换原有的 opt 对象。
opt = argparse.Namespace(**d) # replace
# 重设 opt 对象中的 cfg 、 weights 和 resume 属性, cfg 和 weights 被清空, resume 被设置为True, weights 被设置为最新的检查点路径。
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate
# 检查数据集路径是否是一个URL。
if is_url(opt_data):
# 如果数据集路径是URL,使用 check_file 函数获取有效的文件路径,以避免从Hub恢复时的认证超时。
opt.data = check_file(opt_data) # avoid HUB resume auth timeout
# 如果不需要恢复训练,执行else分支。
else:
# 对 opt 中的 data 、 cfg 、 hyp 、 weights 和 project 路径进行检查,确保它们是有效的文件路径。
# check_file(opt.data) :这个函数检查 opt.data 参数指定的文件或路径是否存在且有效。如果 opt.data 是一个 URL,它会下载文件或确保文件已经被正确下载。这个函数的目的是确保数据文件是可访问的。
# check_yaml(opt.cfg) :这个函数检查 opt.cfg 参数指定的 YAML 配置文件是否存在且有效。 cfg 通常包含了模型的配置信息。 check_yaml 函数会验证 YAML 文件的格式,并确保文件路径是正确的。
# check_yaml(opt.hyp) :类似于 check_yaml(opt.cfg) ,这个函数检查 opt.hyp 参数指定的 YAML 文件,它包含了超参数的配置信息。这个函数确保超参数文件存在且格式正确。
# str(opt.weights) :这个表达式将 opt.weights 参数转换为字符串。这通常用于确保路径是字符串格式,无论它是从命令行参数直接传入的,还是从配置文件中读取的。
# str(opt.project) :与 str(opt.weights) 类似,这个表达式确保 opt.project 参数是字符串格式的,它指定了项目或实验的目录路径。
# def check_yaml(file, suffix=('.yaml', '.yml')): -> 检查一个 YAML 文件是否存在,如果不存在且文件是一个网址,则下载该文件。这个函数还检查文件名后缀是否符合 YAML 文件的标准后缀。 -> return check_file(file, suffix)
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 。
# 如果进行超参数演化,执行以下操作。
if opt.evolve:
# 如果项目的路径是默认的 runs/train ,将其重命名为 runs/evolve 。
if opt.project == str(ROOT / 'runs/train'): # if default project name, rename to runs/evolve
opt.project = str(ROOT / 'runs/evolve')
# 将 resume 的值传递给 exist_ok ,并禁用 resume 。
opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume
# 如果 opt.name 设置为 'cfg' ,使用 cfg 文件的名称作为项目名称。
if opt.name == 'cfg':
# 获取 cfg 文件的名称(不包括扩展名)作为 opt.name 。
opt.name = Path(opt.cfg).stem # use model.yaml as name
# 构造保存目录的路径,并确保目录名是唯一的,如果目录已存在,则增加后缀以避免覆盖。
# def increment_path(path, exist_ok=False, sep='', mkdir=False): -> 为文件或目录生成一个新的路径名,如果原始路径已经存在,则通过在路径后面添加一个数字(默认从2开始递增)来创建一个新的路径。函数返回最终的路径 path 。 -> return path
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))
# 这段代码处理了从检查点恢复训练的逻辑,包括检查和设置训练参数、处理数据集路径、以及在超参数演化时的特殊处理。它确保了训练可以在中断后从正确的状态恢复,并且可以正确地处理数据集和模型的路径。
# 这段代码是用于设置分布式数据并行(Distributed Data Parallel,简称DDP)训练环境的。
# DDP mode
# 调用 select_device 函数来选择一个设备(通常是GPU),并根据 opt.device 参数和批量大小 opt.batch_size 来决定使用哪个设备进行训练。
device = select_device(opt.device, batch_size=opt.batch_size)
# 检查环境变量 LOCAL_RANK 是否不等于 -1 。在分布式训练中, LOCAL_RANK 标识当前进程在本地机器上的编号。
if LOCAL_RANK != -1:
# 定义一个消息字符串,用于在断言失败时提供错误信息。
msg = 'is not compatible with YOLO Multi-GPU DDP training' # 与 YOLO Multi-GPU DDP 训练不兼容。
# 断言 opt.image_weights 为假,因为图像权重( --image-weights )与YOLO的多GPU DDP训练不兼容。
assert not opt.image_weights, f'--image-weights {msg}'
# 断言 opt.evolve 为假,因为超参数演化( --evolve )与YOLO的多GPU DDP训练不兼容。
assert not opt.evolve, f'--evolve {msg}'
# 断言批量大小 opt.batch_size 不等于 -1 ,因为 -1 通常用于AutoBatch模式,而在DDP模式下需要一个有效的批量大小。
assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size'
# 断言批量大小 opt.batch_size 必须是全局大小 WORLD_SIZE (所有GPU的数量)的倍数。
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE'
# 断言本地机器上的CUDA设备数量大于 LOCAL_RANK ,以确保有足够的GPU进行DDP训练。
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command'
# 设置当前进程的CUDA设备为 LOCAL_RANK 指定的设备。
torch.cuda.set_device(LOCAL_RANK)
# 创建一个 torch.device 对象,指定使用CUDA设备和 LOCAL_RANK 编号。
device = torch.device('cuda', LOCAL_RANK)
# 初始化进程组,这是PyTorch分布式训练的必要步骤。这里选择的后端是“nccl”(NVIDIA Collective Communications Library)如果可用,否则使用“gloo”(由Intel开发的用于多GPU和多节点并行计算的库)。
dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
# 这段代码的目的是确保分布式数据并行训练环境正确设置,包括检查不兼容的选项、验证批量大小、设置CUDA设备,并初始化进程组。这些步骤是进行多GPU训练前的重要准备工作。
# 这段代码是用于执行模型训练的逻辑。
# Train
# 条件语句检查 opt.evolve 是否为 False 。 opt.evolve 是一个布尔参数,用于指示是否进行超参数演化(hyperparameter evolution)。如果 opt.evolve 为 True ,则跳过常规训练流程,转而执行超参数演化过程。
if not opt.evolve:
# 如果不进行超参数演化,即 opt.evolve 为 False ,则调用 train 函数来执行模型训练。 train 函数接受以下参数 :
# opt.hyp 超参数配置,可能包含学习率、优化器设置等。 opt 包含训练配置的 argparse.Namespace 对象。 device 用于训练的设备,通常是 GPU。 callbacks 回调函数列表,这些函数可以在训练过程中的特定事件点被调用,例如在每个 epoch 结束时或训练结束时。
train(opt.hyp, opt, device, callbacks)
# 这段代码的目的是判断是否需要进行超参数演化,如果不是,则执行常规的训练流程。 train 函数是模型训练的核心,负责执行训练逻辑,并在训练过程中调用 callbacks 中的回调函数来执行额外的操作,如日志记录、模型保存等。
# 这段代码是用于设置和执行超参数演化(hyperparameter evolution)的逻辑。
# Evolve hyperparameters (optional)
# 这个分支是针对 if not opt.evolve: 条件不成立的情况,即当 opt.evolve 为 True 时执行。
else:
# Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit)
# 定义一个名为 meta 的字典,包含超参数演化的元数据。每个键对应一个超参数,值是一个元组,包含 变异比例 ( mutation scale )、 下界 ( lower limit )和 上界 ( upper limit )。
# 这段代码定义了一个名为 meta 的字典,它包含了用于超参数演化的元数据。这个字典中的每个键代表一个可以调整的超参数,每个值是一个元组,包含了三个元素 :
# 变异比例(Mutation scale) :这个值决定了超参数的变异程度,通常是一个介于0和1之间的数,用于在上下界之间按比例调整超参数值。
# 下界(Lower limit) :这是超参数的最小允许值,用于确保超参数值不会低于某个阈值。
# 上界(Upper limit) :这是超参数的最大允许值,用于限制超参数值不会超过某个上限。
meta = {
# 初始学习率。SGD优化器通常使用1E-2,而Adam优化器通常使用1E-3。
'lr0': (1, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3)
# 最终OneCycleLR学习率,是初始学习率 lr0 的一个比例。
'lrf': (1, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf)
# SGD优化器的动量或Adam优化器的beta1参数。
'momentum': (0.3, 0.6, 0.98), # SGD momentum/Adam beta1
# 优化器的权重衰减参数。
'weight_decay': (1, 0.0, 0.001), # optimizer weight decay
# 预热epochs数,即在这些epochs中逐渐增加学习率。
'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训练阈值,用于确定正样本。
'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)
# 焦点损失的gamma参数。
'fl_gamma': (0, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5)
# hsv_h 、 hsv_s 、 hsv_v :HSV色彩空间中的色调、饱和度、值的增强比例。
'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 语句打开 opt.hyp 指定的 YAML 文件, errors='ignore' 忽略文件读取错误。
with open(opt.hyp, errors='ignore') as f:
# 使用 yaml.safe_load 函数从文件中加载超参数字典。
hyp = yaml.safe_load(f) # load hyps dict
# 检查 hyp 字典中是否存在 anchors 键,如果不存在,则设置默认值。
if 'anchors' not in hyp: # anchors commented in hyp.yaml
# 如果 anchors 键不存在,则设置其值为 3。
hyp['anchors'] = 3
# 检查是否设置了 opt.noautoanchor 选项。
if opt.noautoanchor:
# 如果 opt.noautoanchor 为 True ,则从 hyp 和 meta 字典中删除 anchors 键。
del hyp['anchors'], meta['anchors']
# 设置 opt.noval 和 opt.nosave 为 True ,表示不进行中间验证和保存中间结果,只对最终 epoch 进行验证和保存。 save_dir 设置为 opt.save_dir 的路径对象。
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
# 定义 evolve_yaml 和 evolve_csv 变量,分别用于存储超参数演化的 YAML 文件和 CSV 文件的路径。
evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv'
# 检查是否设置了 opt.bucket 选项。
if opt.bucket:
# 如果设置了 opt.bucket ,则使用 gsutil 命令从 Google Cloud Storage 下载 evolve.csv 文件。
os.system(f'gsutil cp gs://{opt.bucket}/evolve.csv {evolve_csv}') # download evolve.csv if exists
# 这段代码的目的是设置超参数演化的元数据,加载和处理超参数配置文件,并准备超参数演化过程中需要的文件和目录。它还包含了从云端存储下载演化数据的逻辑,以便在本地进行超参数演化。
# 这段代码是超参数演化(hyperparameter evolution)过程中的核心部分,用于选择和变异超参数。
# 循环迭代指定的次数,由 opt.evolve 参数决定,表示要进行的演化代数(generations)。
for _ in range(opt.evolve): # generations to evolve
# 检查是否存在一个名为 evolve.csv 的文件,这个文件包含了之前演化代的结果。
if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate
# Select parent(s) 表明接下来的代码用于选择父代超参数。
# 定义一个变量 parent ,用于指定父代选择方法。这里设置为 'single' ,表示从之前的结果中选择单个最好的超参数集。
parent = 'single' # parent selection method: 'single' or 'weighted'
# 使用 NumPy 的 loadtxt 函数加载 evolve.csv 文件中的数据。 ndmin=2 确保数据被加载为二维数组, delimiter=',' 指定逗号为分隔符, skiprows=1 跳过文件的标题行。
x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1)
# 设置 n 为考虑的先前结果的数量,最多为5个或文件中结果的数量。
n = min(5, len(x)) # number of previous results to consider
# 使用 fitness 函数计算 x 中每行的适应度,并按适应度降序排序,然后选择前 n 个结果。
x = x[np.argsort(-fitness(x))][:n] # top n mutations
# 重新计算选定结果的适应度,并将其最小值减去以确保权重为正,加上 1E-6 防止除以零。
w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0)
# 检查是否选择了单个父代或者只有一个结果可供选择。
if parent == 'single' or len(x) == 1:
# x = x[random.randint(0, n - 1)] # random selection
# 如果是单个父代选择,使用 random.choices 根据权重 w 选择一个索引,并从 x 中选择对应的结果。
x = x[random.choices(range(n), weights=w)[0]] # weighted selection
# 如果选择了加权组合方法。
elif parent == 'weighted':
# 将 x 中的结果与权重 w 相乘,然后按行求和,最后除以权重总和,得到加权平均的超参数集。
x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination
# 这段代码实现了超参数演化中的两个关键步骤:选择和变异。它首先从之前的结果中选择最好的超参数集,然后根据选择方法(单个或加权)来决定新一代的超参数。这个过程模拟了生物进化中的自然选择和遗传变异,目的是在多代演化中找到性能更优的超参数组合。
# 这段代码是超参数演化过程中的变异步骤,用于随机调整选定的超参数。
# Mutate 表明接下来的代码用于变异操作。
# 定义 变异概率 mp 和 标准差 s ,这些参数控制变异的程度。
mp, s = 0.8, 0.2 # mutation probability, sigma
# 将 np.random 模块赋值给 npr 变量,方便后续调用。
npr = np.random
# 设置随机数生成器的种子,确保每次运行代码时生成的随机数不同。
npr.seed(int(time.time()))
# 从 meta 字典中提取每个超参数的变异比例(gains),并将其存储在数组 g 中。
g = np.array([meta[k][0] for k in hyp.keys()]) # gains 0-1
# 计算 meta 字典中的超参数数量,并存储在 ng 中。
ng = len(meta)
# 创建一个所有元素都为1的数组 v ,用于存储变异后的增益值。
v = np.ones(ng)
# 使用 while 循环,直到 v 中的值不再全部为1(即至少有一个超参数发生了变异)。
while all(v == 1): # mutate until a change occurs (prevent duplicates)
# 对每个超参数执行变异操作 :
# npr.random(ng) < mp 产生一个随机数数组,用于确定哪些超参数将被变异(基于变异概率 mp )。
# npr.randn(ng) 产生一个标准正态分布的随机数数组,用于增加变异的随机性。
# npr.random() * s 为每个被选中的超参数添加随机性,控制变异的程度。
# clip(0.3, 3.0) 确保变异后的增益值在0.3到3.0之间。
v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
# 遍历 hyp 字典中的每个超参数。
for i, k in enumerate(hyp.keys()): # plt.hist(v.ravel(), 300)
# 根据变异后的增益值 v 调整超参数的值,并将结果存储回 hyp 字典中。这里 x[i + 7] 表示从之前选择的超参数集 x 中获取原始值, v[i] 是对应的变异增益值。
hyp[k] = float(x[i + 7] * v[i]) # mutate
# 这段代码实现了超参数的变异过程,通过随机调整超参数的值来探索新的超参数组合。变异是超参数演化算法中的关键步骤,它允许算法跳出局部最优解,寻找更广泛的解空间。通过控制变异概率和程度,可以在保持原有超参数集有效性的同时,引入新的超参数组合,从而可能找到性能更优的超参数配置。
# 这段代码是超参数演化过程中的约束步骤,用于确保变异后的超参数值在预定义的范围内,并保留指定的小数位数。
# Constrain to limits 表明接下来的代码用于将超参数值限制在特定的范围内。
# 遍历 meta 字典中的每个项, k 是超参数的名称, v 是对应的元组,包含 变异比例 、 下界 和 上界 。
for k, v in meta.items():
# 将 hyp 字典中对应的超参数值与 meta 字典中定义的下界( v[1] )比较,取较大值。这确保了超参数值不会低于设定的下界。
hyp[k] = max(hyp[k], v[1]) # lower limit
# 将 hyp 字典中对应的超参数值与 meta 字典中定义的上界( v[2] )比较,取较小值。这确保了超参数值不会超过设定的上界。
hyp[k] = min(hyp[k], v[2]) # upper limit
# 将 hyp 字典中对应的超参数值四舍五入到5位小数。这确保了超参数值具有一致的有效数字位数,有助于保持数值的稳定性和可比性。
hyp[k] = round(hyp[k], 5) # significant digits
# 这段代码的目的是确保经过变异后的超参数值仍然在合理的范围内,并且具有一致的小数位数。这是超参数优化中的一个重要步骤,因为某些超参数可能对数值范围非常敏感,超出特定范围可能会导致模型性能显著下降或者训练不稳定。通过限制超参数值,可以保证每次训练都在一个合理的搜索空间内进行,同时也有助于后续对结果的分析和比较。
# 这段代码是超参数演化过程中的“训练变异”步骤,用于训练新变异的超参数集,并记录训练结果。
# Train mutation
# 调用 train 函数,传入变异后的超参数集的副本 hyp.copy() ,原始训练选项 opt ,设备 device 和回调函数 callbacks 。这个函数执行模型训练,并返回训练结果 results 。
results = train(hyp.copy(), opt, device, callbacks)
# class Callbacks:
# -> 用于管理和执行在训练过程中的不同阶段调用的回调函数。
# -> def __init__(self):
# 创建一个新的 Callbacks 实例,用于在新的训练周期中记录和处理回调事件。
callbacks = Callbacks()
# Write mutation results 表明接下来的代码用于记录变异后的训练结果。
# 定义一个元组 keys ,包含需要记录的关键性能指标。
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 。这个函数负责将变异后的训练结果写入文件或数据库,以便于后续分析和比较。
# def print_mutation(keys, results, hyp, save_dir, bucket, prefix=colorstr('evolve: ')): -> 在神经网络架构搜索或超参数优化过程中记录和打印结果。这个函数将结果保存到 CSV 和 YAML 文件中,并在控制台打印当前的进化代数和结果。
print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket)
# 这段代码实现了超参数演化中的“训练变异”步骤,包括使用新的超参数集进行模型训练,并记录关键性能指标。通过这种方式,可以评估每次变异的效果,并选择性能最好的超参数集继续演化。这种迭代过程有助于在多代演化中逐步优化超参数,最终找到最佳的训练配置。
# 这段代码是超参数演化过程结束后的收尾步骤,用于绘制演化结果的图表,并记录关键信息。
# Plot results 表明接下来的代码用于绘制超参数演化的结果图表。
# 调用 plot_evolve 函数,传入包含演化数据的 CSV 文件路径 evolve_csv 。这个函数负责读取 CSV 文件中的数据,并生成图表,展示超参数演化过程中的性能变化。
# def plot_evolve(evolve_csv='path/to/evolve.csv'): -> 用于绘制从 evolve.csv 文件中得到的超参数进化结果。这个函数使用了 Matplotlib 和 Pandas 库来处理数据和绘图。
plot_evolve(evolve_csv)
# 使用日志记录器 LOGGER 的 info 方法记录信息。这里使用了格式化字符串来插入文本和变量。
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}') # 超参数进化完成 {opt.evolve} 代 结果保存至 {colorstr('bold', save_dir)} 使用示例:$ python train.py --hyp {evolve_yaml} 。
# 这段代码完成了超参数演化的最后步骤,包括绘制结果图表和记录关键信息。这有助于用户理解演化过程的效果,并提供了如何使用演化结果的示例。通过这种方式,用户可以轻松地查看演化结果,并根据最优超参数继续进行模型训练。
# 这段代码是一个训练流程的主函数,它处理参数检查、恢复训练、文件检查、分布式训练设置、模型训练以及超参数进化。它通过 train 函数执行实际的训练,并在进化训练中通过变异和选择来优化超参数。最后,它记录训练结果和进化信息。
5.def run(**kwargs):
# 这段代码定义了一个名为 run 的函数,它提供了一种灵活的方式来执行训练程序,并允许从外部传入特定的参数。
# 定义一个函数 run ,它接受任意数量的关键字参数( **kwargs )。这允许调用者在调用 run 函数时传递任意的命名参数。
def run(**kwargs):
# 这是一个注释,提供了如何使用 run 函数的例子。它说明了如何导入 train 模块并调用 run 函数,同时传入数据集配置、图像尺寸和预训练权重路径等参数。
# Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolo.pt')
# 调用 parse_opt 函数,并传入 True 作为参数。这个函数用于解析命令行参数或配置文件,并返回一个包含所有训练选项的 opt 对象。
# def parse_opt(known=False):
# -> 它使用 argparse 库来解析命令行参数。这个函数通常用于配置训练模型时的各种选项。这行代码根据 known 参数的值返回不同的结果。如果 known 为 True ,则返回解析后的已知参数;如果 known 为 False ,则返回解析后的所有参数。
# -> return parser.parse_known_args()[0] if known else parser.parse_args()
opt = parse_opt(True)
# 遍历 kwargs 字典中的所有项, k 是键(参数名), v 是值(参数值)。
for k, v in kwargs.items():
# 对于 kwargs 中的每个参数,使用 setattr 函数将其设置为 opt 对象的属性。这允许从函数外部动态地修改 opt 对象的属性。
setattr(opt, k, v)
# 调用 main 函数,并传入更新后的 opt 对象。这个函数执行实际的训练流程。
main(opt)
# 函数返回 opt 对象,它包含了所有训练选项和可能从 kwargs 更新的参数。
return opt
# run 函数提供了一种便捷的方式来启动训练程序,允许用户通过关键字参数传递特定的配置,而不需要每次都修改配置文件或命令行参数。这种方法增加了训练程序的灵活性和易用性。通过 run 函数,用户可以在代码中直接设置训练参数,并立即开始训练,这对于实验和快速原型设计非常有用。
6.if __name__ == "__main__":
# 这段代码是 Python 程序中常见的模式,用于判断当前脚本是否作为主程序运行。如果是,那么执行其中的代码块。这是一种常用的方法来允许模块被导入时不立即执行某些代码。
# 这是一个条件判断语句。 __name__ 是一个内置变量,当 Python 文件被直接运行时, __name__ 的值为 "__main__" 。如果文件是被导入的,那么 __name__ 的值将是文件名(模块名)。
if __name__ == "__main__":
# 如果当前脚本是作为主程序运行的,那么调用 parse_opt 函数,并把返回的选项对象赋值给变量 opt 。这个函数用于解析命令行参数或者配置文件,并返回一个包含所有训练选项的对象。
opt = parse_opt()
# 接着,将 opt 对象作为参数传递给 main 函数,并执行它。 main 函数是程序的入口点,它调用实际的训练流程。
main(opt)
# 这段代码的目的是确定当前脚本是否作为独立程序运行。如果是,那么解析命令行参数或配置文件,并执行 main 函数来启动训练流程。这种方式使得脚本既可以作为模块被其他脚本导入,也可以直接运行来执行训练任务。