feat(engine): 训练评估循环、CenterNet 损失与指标

- trainer: AMP、梯度裁剪、cosine/step 调度、last/best 检查点,按 monitor 指标挑选 best
- losses: penalty-reduced focal loss(heatmap)+ 掩码 L1(wh/offset)+ 分类损失
  (多类 BCE / 单类 CE,随 label_mode 切换)
- metrics: 定位 IoU 匹配下的 P/R/F1 与 mean IoU;分类多标签 micro-F1 与单标签 accuracy,
  两种模式都输出 cls_f1,故 train.monitor 无需跟着改
- optim: BatchNorm 与 bias 不做 weight decay
- visualizer: SwanLab 记录损失/学习率/验证指标/显存峰值,未安装或离线时降级为警告

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-02 17:55:20 +08:00
co-authored by Claude Opus 5
parent af22dc639c
commit ef64e5bf77
8 changed files with 759 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
"""优化器与学习率调度。"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
import torch
from torch import nn
def build_optimizer(model: nn.Module, cfg: Mapping[str, Any]) -> torch.optim.Optimizer:
"""`cfg` 形如 `{name: sgd, lr: 0.01, momentum: 0.9, weight_decay: 1e-4}`。
BatchNorm 与 bias 不做 weight decay——小数据集上这项影响明显。
"""
cfg = dict(cfg)
name = str(cfg.pop("name", "adamw")).lower()
weight_decay = float(cfg.pop("weight_decay", 0.0))
decay, no_decay = [], []
for module in model.modules():
for param_name, param in module.named_parameters(recurse=False):
if not param.requires_grad:
continue
if param_name == "bias" or isinstance(module, nn.modules.batchnorm._BatchNorm):
no_decay.append(param)
else:
decay.append(param)
groups = [
{"params": decay, "weight_decay": weight_decay},
{"params": no_decay, "weight_decay": 0.0},
]
if name == "sgd":
cfg.setdefault("momentum", 0.9)
cfg.setdefault("nesterov", True)
return torch.optim.SGD(groups, **cfg)
if name == "adam":
return torch.optim.Adam(groups, **cfg)
if name == "adamw":
return torch.optim.AdamW(groups, **cfg)
raise ValueError(f"未知优化器: {name}")
def build_scheduler(optimizer: torch.optim.Optimizer, cfg: Mapping[str, Any] | None, epochs: int):
"""返回按 epoch 步进的 scheduler;`cfg` 为空则不使用。"""
if not cfg:
return None
cfg = dict(cfg)
name = str(cfg.pop("name", "cosine")).lower()
if name == "cosine":
cfg.setdefault("T_max", epochs)
return torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, **cfg)
if name == "step":
cfg.setdefault("step_size", max(epochs // 3, 1))
return torch.optim.lr_scheduler.StepLR(optimizer, **cfg)
if name == "multistep":
cfg.setdefault("milestones", [int(epochs * 0.6), int(epochs * 0.85)])
return torch.optim.lr_scheduler.MultiStepLR(optimizer, **cfg)
raise ValueError(f"未知调度器: {name}")