基于Yolov5+Deepsort+Pyqt的单目标跟踪器设计train.py:

Set DDP variables设置分布式训练参数


world_size:表示参与分布式训练的进程总数(GPU 数量)。从环境变量 WORLD_SIZE 中读取(由分布式框架如 torch.distributed 自动设置),若未设置则默认为 1(单机单卡)。
global_rank:表示当前进程的全局排名(从 0 到 world_size-1)。从环境变量 RANK 中读取,若未设置则默认为 -1(非分布式模式).

resume 恢复

  • opt.data, opt.cfg, opt.hyp = check_file(opt.data), check_file(opt.cfg), check_file(opt.hyp) # check files
    检查文件路径:check_file() 确保 opt.data(数据集配置)、opt.cfg(模型配置)、opt.hyp(超参数文件)存在。
  • assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified'
    assert len(opt.cfg) or len(opt.weights) 确保必须指定 --cfg(模型结构)或 --weights(预训练权重)。
  • opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size)))
    opt.img_size.extend(...) 将 img_size 扩展为 2 个值(训练和测试尺寸相同),例如 [640] → [640, 640]。

DDP Model

  • if opt.local_rank != -1:如果 opt.local_rank 不是 -1,表示启用分布式训练
  • with open(opt.hyp) as f:从 opt.hyp 指定的 YAML 文件中加载超参数。
  • hyp = yaml.load(f, Loader=yaml.FullLoader) # load hyps 使用 yaml.FullLoader 解析 YAML 文件为字典(hyp)。
  • if 'box' not in hyp:检查超参数中是否存在 'box' 键(用于边界框损失计算)。如果不存在,则假设旧版参数 'giou' 存在,并将其重命名为 'box',同时输出兼容性警告。

Train

  • logger.info(opt)使用 logger 打印 opt(训练配置参数),方便用户查看当前的训练设置。
  • if not opt.evolve:检查是否处于超参数进化模式(evolve)。如果不是,则执行后续的 TensorBoard 初始化逻辑。
  • tb_writer = None # init loggers 初始化 tb_writer(TensorBoard 写入器)为 None,后续会根据条件重新赋值。
  • if opt.global_rank in [-1, 0]:检查当前进程的全局排名(global_rank),通常用于分布式训练。-1 表示单机训练,0 表示主进程(在多机多卡训练中)。只有主进程或单机训练时才执行以下操作,避免重复记录。
  • logger.info(f'Start Tensorboard with "tensorboard --logdir {opt.project}", view at http://localhost:6006/')
    打印提示信息,告诉用户如何启动 TensorBoard 服务,并指定日志目录为 opt.project(训练结果保存路径)。用户可以通过访问 http://localhost:6006/ 查看训练过程的可视化结果。
  • tb_writer = SummaryWriter(opt.save_dir) # Tensorboard初始化 SummaryWriter(TensorBoard 的日志写入器),日志将保存到 opt.save_dir(模型和日志的保存目录)。
  • train(hyp, opt, device, tb_writer, wandb)调用
    train函数开始训练,传入以下参数:
    hyp:超参数配置(如学习率、权重衰减等)。
    opt:训练选项(如数据集路径、批次大小等)。
    device:训练设备(如 cuda:0 或 cpu)。
    tb_writer:TensorBoard 写入器,用于记录训练指标。
    wandb:可选参数,用于 Weights & Biases 日志记录(如果启用)。
  • def train(hyp, opt, device, tb_writer=None, wandb=None):
    定义训练函数 train,接收以下参数:
    hyp:超参数配置(如学习率、权重衰减等)。
    opt:训练选项(如数据集路径、批次大小等)。
    device:训练设备(如 cuda:0 或 cpu)。
    tb_writer(可选):TensorBoard 日志写入器,默认为 None。
    wandb(可选):Weights & Biases 日志工具,默认为 None。
  • logger.info(f'Hyperparameters {hyp}')
    使用 logger 打印当前的超参数配置 hyp,方便调试和记录。
  • wdir = save_dir / 'weights'定义权重文件的保存目录(save_dir/weights)。
  • wdir.mkdir(parents=True, exist_ok=True) # make dir
    parents=True:自动创建父目录(如果不存在)。
    exist_ok=True:如果目录已存在,不报错。
  • last = wdir / 'last.pt'保存最后一轮的模型权重。
  • best = wdir / 'best.pt'保存验证集上性能最好的模型权重
  • results_file = save_dir / 'results.txt'保存训练过程中的指标(如损失、精度等)。
  • with open(save_dir / 'hyp.yaml', 'w') as f:将超参数 hyp 保存到 save_dir/hyp.yaml 文件。
  • with open(save_dir / 'opt.yaml', 'w') as f:将训练选项 opt(通过 vars(opt) 转换为字典)保存到 save_dir/opt.yaml 文件。
  • yaml.dump(hyp, f, sort_keys=False)
    sort_keys=False 保持参数原始顺序,不按字母排序。
  • plots = not opt.evolveplots:是否生成训练曲线图(如果是超参数进化模式 evolve,则不生成)。
  • cuda = device.type != 'cpu'检查当前设备是否为 GPU
  • data_dict = yaml.load(f, Loader=yaml.FullLoader)
    从 opt.data(数据集配置文件路径,如 data/coco.yaml)加载 YAML 文件,解析为字典 data_dict。
  • train_path = data_dict['train']
  • test_path = data_dict['val']
    从 data_dict 中获取训练集和验证集的路径
  • nc = 1 if opt.single_cls else int(data_dict['nc'])
    类别数量。如果启用单类别模式(opt.single_cls),则强制设为 1;否则使用 data_dict['nc']。
  • names = ['item'] if opt.single_cls and len(data_dict['names']) != 1 else data_dict['names']
    类别名称列表。如果是单类别模式且配置文件中名称不唯一,则设为 ['item'];否则使用 data_dict['names']。
  • assert len(names) == nc, '%g names found for nc=%g dataset in %s' % (len(names), nc, opt.data)检查类别数量 nc 和名称列表 names 的长度是否一致,否则报错。

Model

  • pretrained = weights.endswith('.pt')通过检查 weights 路径是否以 .pt 结尾,判断是否使用预训练权重
  • with torch_distributed_zero_first(rank):确保分布式训练中,主进程先下载权重,其他进程等待。
  • attempt_download(weights)如果本地不存在 weights 文件,则从网络下载(如从 YOLOv5 官方仓库下载)。
  • ckpt = torch.load(weights, map_location=device)加载 .pt 文件到 ckpt(字典形式),并指定加载到 device(如 cuda:0 或 cpu)。ckpt 通常包含模型权重 state_dict、超参数 hyp、模型配置 yaml 等。
  • if hyp.get('anchors'):如果超参数 hyp 中指定了 anchors,则强制覆盖模型配置中的锚点参数。
  • ckpt['model'].yaml['anchors'] = round(hyp['anchors'])round(hyp['anchors']) 对锚点值取整,确保其为整数。

  • model = Model(opt.cfg or ckpt['model'].yaml, ch=3, nc=nc).to(device)
    如果 opt.cfg(模型配置文件路径)存在,则优先使用;否则使用预训练模型中的配置 ckpt['model'].yaml。
    ch=3:输入图像的通道数(RGB)。
    nc=nc:类别数量(从数据集配置中获取)。
    .to(device):将模型移动到指定设备(GPU/CPU)。
  • exclude = ['anchor'] if opt.cfg or hyp.get('anchors') else []如果使用自定义配置(opt.cfg)或覆盖锚点(hyp['anchors']),则排除 anchor 相关权重(防止冲突)。
  • state_dict = ckpt['model'].float().state_dict()将预训练权重转换为 FP32 格式。
  • state_dict = intersect_dicts(state_dict, model.state_dict(), exclude=exclude)筛选预训练权重中与当前模型结构匹配的部分(忽略不匹配的层)。
  • model.load_state_dict(state_dict, strict=False)加载权重,允许部分加载
  • logger.info('Transferred %g/%g items from %s' % (len(state_dict), len(model.state_dict()), weights))记录成功加载的权重数量(例如 Transferred 300/320 items from yolov5s.pt)。
  • else:model = Model(opt.cfg, ch=3, nc=nc).to(device) # create如果没有提供预训练权重(pretrained=False),则直接根据 opt.cfg 创建新模型,并随机初始化权重。

Freeze

  • freeze = []定义一个空列表 freeze,用于存储需要冻结的层名称。用户可以通过修改 freeze 列表(例如 freeze = ['backbone'])来冻结模型的某些部分。
  • for k, v in model.named_parameters():遍历模型中所有可训练参数,k 是参数名称(如 model.0.conv.weight),v 是参数张量。
  • v.requires_grad = True默认将所有参数的 requires_grad 设为 True,表示在训练时更新这些参数的权重。默认启用梯度计算
  • if any(x in k for x in freeze): print('freezing %s' % k) v.requires_grad = False检查当前参数名称 k 是否包含 freeze 列表中的任何子字符串(例如 k 包含 backbone)。
    如果匹配,则打印冻结日志,并将该参数的 requires_grad 设为 False(冻结权重,不参与梯度更新)
    若 freeze = ['backbone'],则所有名称中包含 backbone 的层(如backbone.conv1.weight)会被冻结。

optimizer 优化器

  • nbs = 64 # nominal batch size名义批次大小(Nominal Batch Size),默认为 64,作为基准值。
  • accumulate = max(round(nbs / total_batch_size), 1)梯度累积步数。当实际总批次大小(total_batch_size,如多卡训练的全局批次)小于 nbs 时,通过多次前向-反向传播累积梯度,再更新权重(等效增大批次大小)。
  • hyp['weight_decay'] *= total_batch_size * accumulate / nbs根据实际批次大小调整权重衰减系数,保持与名义批次的一致性。
  • pg0, pg1, pg2 = [], [], []
    将模型参数分为三组,用于差异化优化策略:
    pg0:BatchNorm2d 层的权重(不应用权重衰减)。
    pg1:普通卷积层的权重(应用权重衰减)。
    pg2:所有层的偏置参数(不应用权重衰减)。
  • if opt.adam:
    如果使用 Adam 优化器(opt.adam=True):
    初始化 Adam,设置学习率 lr0,并将 beta1 替换为超参数 momentum。
    默认使用 SGD 优化器:
    初始化 SGD,设置学习率 lr0、动量 momentum,并启用 Nesterov 加速。
  • optimizer.add_param_group({'params': pg1, 'weight_decay': hyp['weight_decay']})
  • optimizer.add_param_group({'params': pg2})
    将分组参数添加到优化器:
    pg1(卷积权重)附带权重衰减。
    pg2(偏置)不附带权重衰减。
  • lf = one_cycle(1, hyp['lrf'], epochs)one_cycle:生成一个余弦退火学习率调度函数,初始值为 1,最终值降至 hyp['lrf']
  • scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)
    LambdaLR:将调度函数 lf 应用到优化器,实现学习率动态调整。

Resume


start_epoch:初始化为 0,表示训练从第 0 个 epoch 开始。
best_fitness:初始化为 0.0,用于记录模型的最佳性能指标
如果 pretrained 为 True,表示需要加载预训练模型的参数。
ckpt 是预训练模型的检查点(checkpoint)字典。
如果检查点中包含优化器状态(ckpt['optimizer'] 不为 None),则将其加载到当前优化器(optimizer)中,以恢复训练时的优化器参数(如动量、学习率等)。
从检查点中加载之前保存的最佳性能指标(best_fitness),用于后续模型保存和早停(early stopping)等逻辑。

Results


ckpt 是预训练模型的检查点字典。使用 ckpt.get('training_results') 检查检查点中是否包含键 'training_results'(历史训练结果,如损失、精度等指标)。如果存在(不为 None),则执行后续操作。
results_file 是保存训练结果的文件路径(默认是 results.txt)。以写入模式('w')打开文件,并将检查点中的 training_results内容写入文件中。这样做的目的是恢复之前的训练记录,方便后续继续训练时能够跟踪完整的训练历史。

Epochs


从检查点 ckpt 中加载之前训练到的 epoch 值(ckpt['epoch']),并 +1 作为本次训练的起始 epoch。
如果用户通过命令行参数 --resume 显式要求恢复训练:
使用 assert 检查 start_epoch 是否大于 0。如果为 0,说明之前已经完成了全部 epochs 的训练,无需恢复,直接报错提示。
错误消息示例:"model.pt training to 200 epochs is finished, nothing to resume."
如果用户设置的 epochs(本次训练的总 epoch 数)小于 start_epoch(恢复的起始 epoch):
说明用户可能想继续微调(fine-tune)模型,因此将总 epoch 数调整为 原 epochs + 已训练的 epoch 数。
例如:检查点已训练 50 个 epoch,用户设置 epochs=10,则实际总 epoch 数为 50 + 10 = 60。

  • del ckpt, state_dict删除检查点 ckpt 和模型状态字典 state_dict,释放内存资源。

Image size

  • gs = int(model.stride.max())
    model.stride 是模型各层的下采样步长(如 [8, 16, 32])。
    max() 取最大步长(如 32),并转换为整数 gs。
    作用:gs 表示特征图的最小网格尺寸(grid size),用于后续调整图像尺寸对齐。
  • nl = model.model[-1].nl
    model.model[-1] 是模型的最后一层(检测头,如 Detect 层)。
    nl 是检测头的层数(YOLOv5 默认为 3,对应 3 种尺度的检测输出)。
  • imgsz, imgsz_test = [check_img_size(x, gs) for x in opt.img_size]
    opt.img_size 是用户设置的训练和测试图像尺寸(如 [640, 640])。
    check_img_size(x, gs) 是一个函数,确保图像尺寸 x 是 gs 的整数倍(避免特征图尺寸出现小数)
    输出:
    imgsz:调整后的训练图像尺寸。
    imgsz_test:调整后的测试图像尺寸。
    假设 gs=32,用户设置 img_size=640:
    检查 640 % 32 == 0(满足条件,直接返回 640)。
    如果用户设置 img_size=641,则自动向下舍入到最近的合法值 640。

DP model 数据并行(Data Parallelism)


cuda:布尔值,表示是否启用 CUDA(即是否使用 GPU)。
rank == -1:rank 是分布式训练的进程编号,-1 表示当前是非分布式训练(单机多 GPU)。
torch.cuda.device_count() > 1:检测当前系统中可用的 GPU 数量是否大于 1。
条件满足时:执行 DataParallel 包装,启用多 GPU 数据并行。

SyncBatchNorm 同步批归一化


opt.sync_bn:布尔值,表示用户是否通过命令行参数 --sync-bn 显式启用了同步批归一化。
cuda:布尔值,表示是否使用 GPU 训练。
rank != -1:rank 是分布式训练的进程编号,-1 表示单机单 GPU 训练,非 -1 表示当前是分布式训练(如多 GPU 或多节点)

EMA 创建指数移动平均

  • ema = ModelEMA(model) if rank in [-1, 0] else None
    ModelEMA(model):这是一个封装类,用于计算模型的指数移动平均(EMA)。
    EMA 是一种平滑模型参数的技术,可以减少训练过程中的波动,提升模型的泛化能力。
    它会维护一个影子模型(shadow model),在每次参数更新时,按一定衰减率(如 0.999)更新影子模型的参数。
    rank in [-1, 0]:rank 表示当前进程的编号,用于分布式训练(如 torch.distributed 或 multiprocessing)。
    rank = -1:单机单卡(非分布式训练)。
    rank = 0:分布式训练的主进程(负责日志、保存模型等)。

Trainloader 创建数据加载器


train_path:训练数据路径(如 data.yaml 中定义的路径)。
imgsz:输入图像的尺寸(如 640)。
batch_size:每个批次的样本数。
gs:模型步长(grid size),用于确保图像尺寸能被 gs 整除。
opt:命令行参数对象(包含超参数配置)。
hyp:超参数字典(如学习率、数据增强参数等)。
augment=True:启用数据增强(如翻转、缩放等)。
cache=opt.cache_images:是否缓存图像到内存(加速后续训练)。
rect=opt.rect:是否使用矩形训练(减少填充,提升效率)。
rank:当前进程编号(分布式训练时使用)。
world_size:总进程数(分布式训练时使用)。
workers:数据加载的子进程数。
image_weights:是否按样本权重采样(解决类别不平衡)。
quad:是否使用四合一拼接数据增强(Mosaic 增强的变种)。

  • mlc = np.concatenate(dataset.labels, 0)[:, 0].max()从数据集的标签中提取所有类别编号,并计算最大值 mlc(Max Label Class)。
    dataset.labels:数据集中所有图像的标签列表(每个标签的格式为 [class_id, x_center, y_center, width, height])。
    np.concatenate(dataset.labels, 0):将所有标签拼接成一个二维数组。
    [:, 0]:提取所有标签的类别编号(第 0 列)。
    .max():计算类别编号的最大值。

process 0

  • plot_labels(labels, save_dir, loggers) 生成 labels.jpg 文件,显示类别频率和框的宽高比。
  • check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz)
    根据训练数据集的标签,自动优化模型的锚框尺寸。
    hyp['anchor_t']:锚框匹配阈值(超参数,默认 4.0)。
    imgsz:输入图像尺寸。

model parameter

  • hyp['cls'] *= nc / 80. 根据实际类别数 nc 调整分类损失权重 hyp['cls'](默认基于 COCO 数据集的 80 类)。
  • hyp['obj'] *= imgsz ** 2 / 640. ** 2 * 3. / nl根据输入图像尺寸 (imgsz) 和输出层数 (nl) 调整目标存在损失权重 hyp['obj']。

start train

  • t0 = time.time() 记录训练开始的时间戳,用于后续计算总训练耗时。
  • nw = max(round(hyp['warmup_epochs'] * nb), 1000)确定学习率热身(Warmup)阶段的迭代次数。
    hyp['warmup_epochs']:热身轮次数(默认 3 轮)。
    nb:每个 epoch 的批次数量(len(dataloader))。
    max(..., 1000):至少热身 1000 次迭代(避免小数据集热身不足)。
  • maps = np.zeros(nc)存储每个类别的 mAP(初始为 0)。
  • results = (0, 0, 0, 0, 0, 0, 0)
    初始化评估结果元组,包含:
    P:精确率(Precision)
    R:召回率(Recall)
    mAP@.5:IoU 阈值为 0.5 的 mAP
    mAP@.5-.95:IoU 阈值从 0.5 到 0.95 的平均 mAP
    val_loss:验证损失(边界框、目标存在、分类损失)
  • scheduler.last_epoch = start_epoch - 1设置学习率调度器的起始轮次,last_epoch 必须比当前轮次小 1,否则调度器会跳过第一轮调整。
  • scaler = amp.GradScaler(enabled=cuda) 启用混合精度训练,enabled=cuda:仅在 GPU 训练时启用。

Update image weights (optional)

mean losses

  • mloss = torch.zeros(4, device=device)初始化一个长度为 4 的张量,用于累积当前 epoch 的四种损失均值:
    mloss[0]:边界框损失(box_loss)
    mloss[1]:目标存在损失(obj_loss)
    mloss[2]:分类损失(cls_loss)
    mloss[3]:总损失(total_loss)
  • pbar = enumerate(dataloader)
  • logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', ``'total', 'targets', 'img_size'))
    日志输出示例:
    Epoch gpu_mem box obj cls total targets img_size
    gpu_mem:GPU 显存占用(GB)
    box/obj/cls/total:各项损失值
    targets:当前批次的目标数量
    img_size:图像尺寸
  • pbar = tqdm(pbar, total=nb)
    仅在主进程(单卡或 rank=0)显示进度条,避免多进程日志混乱。
  • for i, (imgs, targets, paths, _) in pbar:
    imgs:批次的输入图像(形状为 [batch_size, 3, H, W])
    targets:标注信息(形状为 [num_targets, 6],每行格式为 [batch_index, class_id, x, y, w, h])
    paths:图像文件路径(用于调试)
  • ni = i + nb * epoch累计迭代次数 ni:用于学习率热身和日志记录。
  • imgs = imgs.to(device, non_blocking=True).float() / 255.0
    .to(device):将数据转移到 GPU(若 cuda=True)。
    non_blocking=True:异步传输,提升数据加载效率。
    / 255.0:将像素值从 [0, 255] 归一化到 [0.0, 1.0]。

Warm up

  • accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round())nbs:名义批次大小(nominal batch size,如 64)。
    total_batch_size:实际批次大小(单卡 batch_size × 卡数)。

Multi scale

Forward Backward optimize

  • with amp.autocast(enabled=cuda):启用自动混合精度(Automatic Mixed Precision, AMP)训练(如果CUDA可用),以减少显存占用并加速计算。amp.autocast:自动将部分操作转换为低精度(FP16),同时保持关键部分的精度(FP32)。
  • pred = model(imgs)将输入图像 imgs 输入模型,得到预测结果 pred。
    model(imgs):调用模型的前向传播方法。
  • loss, loss_items = compute_loss(pred, targets.to(device), model)计算损失函数,返回总损失 loss 和各部分损失项 loss_items。
    compute_loss:YOLOv5的损失函数,包括分类损失、边界框损失和置信度损失。
    targets.to(device):将标注数据(目标)移动到与模型相同的设备(如GPU)。
  • loss *= opt.world_size如果使用分布式训练(DDP模式),将损失乘以 world_size(GPU数量),以便后续梯度平均。
    rank:当前进程的排名(-1表示单机训练)。
    opt.world_size:参与训练的GPU总数。
  • scaler.scale(loss).backward()使用梯度缩放(Gradient Scaling)执行反向传播。
    scaler.scale(loss):在混合精度训练中,缩放损失值以避免梯度下溢。
    .backward():计算梯度。
  • if ni % accumulate == 0:每隔 accumulate 步(梯度累积步数)执行一次参数更新。
    ni:当前迭代次数。
    accumulate:梯度累积的步数(模拟更大的batch size)。
  • scaler.step(optimizer)更新模型参数(优化器步进)。
    scaler.step:在混合精度训练中,先反缩放梯度,再调用 optimizer.step()。
  • scaler.update()调整梯度缩放因子(根据梯度动态调整缩放比例)
  • optimizer.zero_grad()清空优化器的梯度缓存。
  • ema.update(model)如果启用指数移动平均(EMA),更新EMA模型的参数。
    ema.update(model):将当前模型的参数按一定权重(动量)更新到EMA模型中,用于提升模型泛化性。

Print plot

  • mloss = (mloss * i + loss_items) / (i + 1) 计算并更新平均损失(mloss)。
    mloss:历史平均损失的滑动平均值。
    loss_items:当前批次的各项损失值(分类、边界框、置信度等)。
    i:当前批次索引(用于滑动平均计算)
  • mem = '%.3gG' % (torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0)获取当前GPU显存占用(单位:GB)。
    torch.cuda.memory_reserved():返回已分配的显存总量。
    1E9:转换为GB单位。
  • s = ('%10s' * 2 + '%10.4g' * 6) % ('%g/%g' % (epoch, epochs - 1), mem, *mloss, targets.shape[0], imgs.shape[-1])格式化训练日志字符串,包括:
    epoch/epochs-1:当前轮次/总轮次(如 1/300)。
    mem:显存占用。
    *mloss:展开平均损失值(分类、边界框、置信度等)。
    targets.shape[0]:当前批次的标注目标数量。
    imgs.shape[-1]:输入图像的尺寸(如 640)。
  • pbar.set_description(s)更新进度条(tqdm)的显示信息,实时展示训练状态。
  • Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start()启动一个后台线程(Thread)调用 plot_images 函数,绘制当前批次的图像和标注框。
    imgs:输入图像张量。
    targets:标注数据(类别、边界框)。
    paths:图像文件路径(用于显示文件名)。
    daemon=True:线程随主进程退出而终止。

模型评估 MAP

  • ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'gr', 'names', 'stride', 'class_weights'])
    如果启用了指数移动平均(EMA),则更新 EMA 模型的属性(如配置文件、类别数、超参数等),确保 EMA 模型与原始模型同步关键参数。
  • final_epoch = epoch + 1 == epochs判断当前 epoch 是否是最后一个 epoch(epoch + 1 等于总 epoch 数 epochs)。
  • if not opt.notest or final_epoch: 如果未设置 --notest 参数(即需要测试)或当前是最终 epoch,则执行测试逻辑。
  • results, maps, times = test.test调用 test.py 中的 test() 函数计算 mAP,传入以下参数:
    opt.data:数据集配置文件路径(如 data/coco.yaml)。
    batch_size:测试时的批次大小。
    imgsz:测试图像尺寸。
    model:使用 EMA 模型(如果启用)进行推理。
    single_cls:是否单类别检测(由 --single-cls 参数控制)。
    dataloader:测试数据加载器。
    save_dir:结果保存目录。
    plots:仅在最终 epoch 时生成评估图表(如 PR 曲线、混淆矩阵等)。
    log_imgs:如果启用 WandB 日志,记录测试图像数量(由 --log-imgs 控制)。
    输出结果:
    results:包含精度(precision)、召回率(recall)、mAP@0.5、mAP@0.5:0.95 等指标。
    maps:各类别的 mAP 值。
    times:推理耗时统计。

Write/Log/Update best mAP

  • with open(results_file, 'a') as f:results_file:存储评估结果的文本文件路径(如 runs/train/exp/results.txt)。
    'a' 模式:以追加(append)方式打开文件,保留历史记录。
  • f.write(s + '%10.4g' * 7 % results + '\n') # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls)
    '%10.4g' * 7 % results:将 results 中的 7 个数值格式化为固定宽度(10 字符)、4 位有效数字的浮点数。通常对应:
    Precision (P)
    Recall (R)
    mAP@0.5
    mAP@0.5:0.95
    验证损失(box, obj, cls)
    '\n':换行符,确保每次写入的结果单独成行。
  • if len(opt.name) and opt.bucket:opt.name:实验名称(非空时触发上传)。
    opt.bucket:GCS 存储桶名称(如 my-bucket)。
  • os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name))使用 gsutil cp 命令将本地文件 results_file 复制到 GCS 路径 gs://{bucket}/results/results{name}.txt。
    例如:若 opt.name='exp1' 且 opt.bucket='my-bucket',文件会被上传到 gs://my-bucket/results/results_exp1.txt。
  • tags =
    训练损失:
    train/box_loss(边界框回归损失)、
    train/obj_loss(目标置信度损失)、
    train/cls_loss(分类损失)。
    评估指标:
    metrics/precision(精度)
    metrics/recall(召回率)
    metrics/mAP_0.5(mAP@0.5)
    metrics/mAP_0.5:0.95(mAP@0.5:0.95)。
    验证损失:
    val/box_loss
    val/obj_loss
    val/cls_loss(与训练损失对应,但基于验证集计算)。
    学习率:
    x/lr0、x/lr1、x/lr2 分别对应优化器中不同参数组的学习率
  • for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags):
    mloss[:-1]:训练损失的均值(排除可能存在的额外统计项)。
    results:评估指标(如 precision、recall、mAP 等,来自 test.test() 的返回值)。
    lr:学习率列表(对应不同参数组)。
    通过 zip() 将数值 (x) 和标签 (tag) 一一配对。
  • tb_writer.add_scalar(tag, x, epoch) # tensorboard将数值 x 记录到 TensorBoard,标签为 tag,横轴为 epoch。
  • fi = fitness(np.array(results).reshape(1, -1))
    results 是一个包含模型评估指标的列表(如精确度 P、召回率 R、mAP@0.5、mAP@0.5-0.95 等)。
    np.array(results).reshape(1, -1) 将 results 转换为一个 NumPy 数组,并调整形状为 (1, n),其中 n 是原始 results 的长度。
    fitness() 是一个自定义函数,用于计算模型的适应度值。通常,适应度是这些指标的加权组合(如 0.1P + 0.9R 或其他权重配置),用于综合评估模型性能。
  • if fi > best_fitness:比较当前适应度 fi 和历史最佳适应度 best_fitness。
    如果当前适应度更高,说明模型性能有所提升。
  • best_fitness = fi更新 best_fitness 为当前更高的适应度值 fi,用于后续比较和模型保存逻辑。

save model

  • save = (not opt.nosave) or (final_epoch and not opt.evolve)根据命令行参数 opt.nosave 和训练阶段(是否最后一轮 final_epoch 或是否在超参数进化模式 opt.evolve)判断是否需要保存模型。
    opt.evolve:是否处于超参数进化模式(一种自动化调参方法)。
  • with open(results_file, 'r') as f:读取训练结果文件
  • torch.save(ckpt, last)保存最后一次训练的模型
  • if best_fitness == fi:
    torch.save(ckpt, best)条件 best_fitness == fi 表示当前模型的适应度(fi)达到了历史最佳(best_fitness)。如果当前适应度是历史最佳,额外保存为最佳模型

Strip optimizers 移除优化器


检查当前进程:仅在主进程(rank 为 -1 或 0)执行操作。
选择最终模型:优先使用性能最佳的模型(best.pt),若不存在则使用最后一次训练的模型(last.pt)
移除优化器状态:对 last.pt 和 best.pt 文件调用 strip_optimizer(),以减小文件体积。
上传模型到云存储:如果指定了 Google Cloud Storage 的存储桶(opt.bucket),将最终模型上传。

Plots

Test best.pt

  • logger.info('%g epochs completed in %.3f hours.\n' % (epoch - start_epoch + 1, (time.time() - t0) / 3600))
    记录训练完成的 epoch 数量和总耗时。
    epoch - start_epoch + 1:计算实际训练的 epoch 数量(从 start_epoch 到当前 epoch)。
    (time.time() - t0) / 3600:将训练时间(秒)转换为小时,保留 3 位小数。
    通过 logger.info 输出日志,例如:"10 epochs completed in 2.345 hours."
  • dist.destroy_process_group()
    在分布式训练(多 GPU/多节点训练)结束时,清理并释放进程组资源。
    如果训练时使用了 PyTorch 的分布式训练(如 torch.distributed),需要在训练结束后调用此函数,以避免资源泄漏或进程残留。
  • torch.cuda.empty_cache()
    释放 PyTorch 在 CUDA 设备(GPU)上未使用的缓存内存。
    在训练或推理过程中,PyTorch 会缓存一部分 GPU 内存以加速后续操作(如张量分配、计算等)。
    调用此函数会强制释放这些缓存,但不会影响已分配给张量的显存。
赞赏