Files
ModelTrainEval/README.md
T

363 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ModelTrainEval
薄膜缺陷**分类 + 定位**模型的训练与评估。模型最终部署在实时设备上,因此只使用基于
convolution 的结构(不含 attention / transformer),并支持导出 ONNX。
## 目录
| 目录 | 作用 |
| --- | --- |
| `configs/` | yaml 配置(`model/` 模型、`dataset/` 数据、`train/` 训练、`eval/` 评估) |
| `models/` | 模型骨架:`backbones/` + `necks/` + `heads/`,由 `defect_net.py` 组装 |
| `datasets/` | 抛出数据样本的 Dataset 类、数据增强、监督信号编码 |
| `data/` | 数据集本体或软链接(不入库,见 [data/README.md](data/README.md)) |
| `scripts/` | 格式转换、数据集切分、ONNX 导出等批处理脚本 |
| `engine/` | 训练 / 评估循环、损失、指标、优化器、检查点、SwanLab 可视化 |
| `utils/` | 配置、注册表、日志、随机种子、设备解析(cuda/mps/cpu) |
| `tests/` | 单元测试 |
| `docs/` | 算法与数学说明文档 |
| `figures/` | 文档用的 mermaid 图(`.mmd` 源 + `.md` 预览 + `.png`) |
入口在仓库根目录:[train.py](train.py)、[eval.py](eval.py)。
## 环境
```bash
uv sync # 安装依赖(含 swanlab)
uv sync --group export # 需要导出 ONNX 时再装
uv run python scripts/check_env.py # 自检(Python / PyTorch / 可用后端)
```
### 计算后端
支持 **CUDA / MPS(Apple Silicon)/ CPU**。配置里的 `device: auto` 按 `cuda > mps > cpu`
自动挑最快的可用后端,也可以显式写 `cuda` / `mps` / `cpu`,或用命令行 `--device` 覆盖:
```bash
uv run python train.py --config configs/train/baseline.yaml # auto
uv run python train.py --config configs/train/baseline.yaml --device mps # 强制 Mac GPU
uv run python train.py --config configs/train/baseline.yaml --device cpu
```
**显式指定不可用的后端会直接报错退出**,不会悄悄退回 CPU——训练慢几十倍却不吭声是最坏的情况。
| 后端 | AMP 默认 | 显存统计 | 备注 |
| --- | --- | --- | --- |
| `cuda` | **开** | 精确峰值 | Linux 上 PyPI 的 torch 即 CUDA 构建;指定版本用 `uv pip install --index-url https://download.pytorch.org/whl/cu124 torch torchvision` |
| `mps` | **关** | 采样下界 | Apple Silicon。autocast 可用且数值正常,但实测比 fp32 慢(tiny_conv @128²:fp32 76 ms/步,fp16 90 ms),故默认不开;`amp: true` 可强制打开 |
| `cpu` | 不支持 | 无 | 实测比 MPS 慢约 30 倍,仅适合调试与精度回归 |
`amp` 字段接受 `auto`(随后端)/ `true` / `false`。MPS 上 `torch.profiler` 抓不到 Metal kernel
(没有对应的 `ProfilerActivity`),算子明细只反映 CPU 侧下发耗时。
## 数据准备
标注为 **YOLO 风格**:`images/` 下每张图在同层 `labels/` 有一个同名 `.txt`,
每行 `class_id cx cy w h`(相对宽高归一化到 [0,1]),空文件或缺失 `.txt` 即无缺陷样本。
```
data/film/
├── images/{train,val}/0001.png
├── labels/{train,val}/0001.txt
└── data.yaml # path / train / val / nc / names
```
```bash
# 1. labelme / VOC -> YOLO txt(顺带生成 data.yaml)
uv run python scripts/convert_annotations.py --format labelme \
--input data/film/labelme --images data/film/images \
--output data/film/labels --data-yaml data/film/data.yaml
# 2. 切分 train / val(默认建软链,不复制数据)
uv run python scripts/split_dataset.py --images data/film/images \
--labels data/film/labels --val-ratio 0.2 --data-yaml data/film/data.yaml
```
配置里的 `classes` 既可以直接写类别列表,也可以写成 `data/film/data.yaml`,由框架读 `names`。
读取时也接受 YOLO 分割格式的多边形行(取外接矩形)。细节见 [data/README.md](data/README.md)
与 [datasets/yolo.py](datasets/yolo.py)。
## 训练与评估
```bash
uv run python train.py --config configs/train/baseline.yaml
uv run python train.py --config configs/train/baseline.yaml --set train.epochs=10 train.optimizer.lr=5e-4
uv run python train.py --config configs/train/baseline.yaml --resume runs/baseline/last.pth
uv run python eval.py --config configs/eval/baseline.yaml --checkpoint runs/baseline/best.pth
uv run python scripts/export_onnx.py --config configs/train/baseline.yaml \
--checkpoint runs/baseline/best.pth --output runs/baseline/model.onnx
```
产物写在 `work_dir`(默认 `runs/<name>/`):`train.log`、`config.yaml`、`last.pth`、`best.pth`、`swanlog/`。
## 多类 / 单类分类
图像级分类的形式由 yaml 的 `label_mode` 决定,模型的分类头、分类损失、分类指标都跟着切换
(定位分支不受影响,始终按 `classes` 逐类输出 heatmap):
| `label_mode` | 含义 | 分类头输出 | 损失 | 主要指标 |
| --- | --- | --- | --- | --- |
| `multilabel`(默认) | **多类**:一张图可同时含多种缺陷,全 0 表示 OK 片 | `len(classes)` | 逐类 BCE | micro P/R/F1、`cls_exact_match` |
| `multiclass` | **单类**:一张图只归一个类别,互斥 | `len(classes) + 1`(末位为"无缺陷") | softmax CE | `cls_accuracy`、macro P/R/F1 |
```bash
uv run python train.py --config configs/train/baseline.yaml # 多类
uv run python train.py --config configs/train/multiclass.yaml # 单类
uv run python train.py --config configs/train/baseline.yaml --set label_mode=multiclass
```
单类模式下若一张图标注了多种缺陷,取框数最多的类别(并列取下标最小的);
无缺陷样本落到背景下标 `len(classes)`。两种模式的分类指标都含 `cls_f1`,
因此 `train.monitor` 不必跟着改。
## 训练可视化(SwanLab)
训练过程通过 [SwanLab](https://swanlab.cn) 记录:按 step 记录各项损失与学习率,按 epoch 记录
验证指标与显存峰值,实验的超参就是完整的 yaml 配置。在 `train.swanlab` 里配置:
```yaml
train:
swanlab:
enabled: true
project: film-defect
name: null # 缺省用 work_dir 目录名
mode: online # online(云端) | local(本地看板) | offline(仅存盘) | disabled
tags: [tiny_conv, center-head]
```
```bash
# 本地看板:不需要账号,训练时用 local 模式,另开终端起看板
uv run python train.py --config configs/train/baseline.yaml --set train.swanlab.mode=local
uv run swanlab watch runs/baseline/swanlog # 默认 http://127.0.0.1:5092
# 云端
uv run swanlab login # 首次使用需登录
uv run python train.py --config configs/train/baseline.yaml # mode 默认 online
# 离线跑完后再补传云端
uv run swanlab sync runs/baseline/swanlog
```
看板可以在训练**进行中**打开,指标边训边刷。端口冲突用 `-p 5100`,要局域网访问加 `-h 0.0.0.0`。
本地看板依赖 `swanlab[dashboard]`,已包含在 `uv sync` 里。
未安装 swanlab、未登录或网络不通时会打一条警告并继续训练,不会中断。关掉用
`--set train.swanlab.enabled=false`。
## 推理性能与显存分析
部署前用 `torch.profiler` 看单帧耗时与显存占用:
```bash
uv run python scripts/profile_inference.py --config configs/train/baseline.yaml \
--checkpoint runs/baseline/best.pth --batch-size 1 4 8 --postprocess
# 导出 chrome trace,用 chrome://tracing 或 ui.perfetto.dev 打开
uv run python scripts/profile_inference.py --config configs/train/baseline.yaml \
--checkpoint runs/baseline/best.pth --trace runs/baseline/trace.json --out runs/baseline/profile.json
```
输出包含:每个 batch 的单帧延迟 / FPS、显存峰值、权重占用,以及按显存和耗时各排一次序的
算子级明细。`--amp` 用 fp16 推理对比,`--postprocess` 把 heatmap 解码也计入,
`--device` 指定后端(默认 auto)。
后端差异见[计算后端](#计算后端):CUDA 给精确峰值;MPS 上 `torch.profiler` 抓不到 Metal kernel,
算子明细只反映 CPU 侧下发耗时、显存靠采样是下界;CPU 只有耗时与 CPU 内存。
## 未分类样本的无监督聚类
### 调用 ImageNet 预训练模型
`data/film` 里大量样本还没人工分完。这条流水线把目录下**所有图片拉平**(不看任何
目录名),做无监督聚类,产出可人工核对的分组:
```bash
# ImageNet 预训练卷积特征,不训练,CPU 上 14.5k 张约 100 秒
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml
# 先扫一遍簇数再决定分几类
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --sweep-k 2-8
# 换成在本地数据上自监督训练的特征(SimCLR,Mac 上自动走 MPS)
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml --set method=simclr
```
簇数由配置的 `num_clusters` 决定,也可以 `--num-clusters 6` 临时覆盖(实测 4 类的换种子稳定性最高,见文档 §10.4)。产出在 `work_dir`:
`assignments.csv`(每张图的簇号与到质心的距离)、`summary.json`、
`clusters/cluster_XX/`(按簇建的软链目录,直接用看图软件翻)、
`cluster_XX_preview.png`(每簇最靠近质心的代表样本拼图)。
翻图时注意 `clusters/` 里文件名前缀是排序号:`0000_` 最靠近质心(最典型),
末尾 20 张是离质心最远的离群样本——最能暴露这个簇是不是混了东西。
**尺寸不统一的处理**:这批图是已裁好的缺陷小图,中位 152×113,宽高比从 0.24 到 39.9。
尺寸和宽高比是上游 ROI 裁剪的产物、不是缺陷属性,所以预处理的目标是把它抹掉:
直接拉伸到 128×128(不用 letterbox——填充比例等价于把宽高比明着喂给网络),
再做逐图 z-score 抹掉整体亮度/对比度。
无标签时用两个指标判断质量:**轮廓系数**(簇是否紧致)和**换随机种子的稳定性**
(同一份数据换初始化后划分是否还一样,低说明是在硬切一团连续数据)。
> 完整的数学推导、算法流程图与全部消融数据见
> **[docs/unsupervised-clustering.md](docs/unsupervised-clustering.md)**。
### 自监督预训练(SimCLR)
`method: pretrained`(默认)用 ImageNet 权重直接抽特征,不训练;`method: simclr` 则在
**本批薄膜图上自监督训练**一个项目自己的纯卷积骨架,域适配更好。同一张图的两个随机增强
视图互为正样本,batch 内其余 2B−2 个视图为负样本,用 NT-Xent 损失训练。
训练走 `device: auto` 选中的后端——**Mac 上直接用 MPS 即可**,实测 14,579 张、
batch 256 时约 65 秒/epoch。
#### 第一步:训练 + 聚类(一条命令)
`method=simclr` 会先训练、存权重,然后直接用这份权重抽特征做聚类,不需要分两次调用:
```bash
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml \
--work-dir runs/simclr \
--set method=simclr simclr.epochs=40 loader.num_workers=8
```
按 65 秒/epoch 算,40 轮约 **45 分钟**。产出:
```
runs/simclr/
├── simclr_backbone.pth # 只含骨架,给下游用(聚类 / 初始化 DefectNet)
├── simclr_last.pth # 完整训练状态,给断点续训用
├── swanlog/ # SwanLab 日志
├── cluster.log
├── assignments.csv
├── summary.json
├── clusters/cluster_XX/
└── cluster_XX_preview.png
```
跑长任务建议丢后台,随时能看进度:
```bash
nohup uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml \
--work-dir runs/simclr \
--set method=simclr simclr.epochs=40 loader.num_workers=8 > runs/simclr.log 2>&1 &
tail -f runs/simclr.log
```
#### 断点续训
训练中断(Ctrl-C、kill、机器重启)后接着练,用 `simclr.resume` 指向 `simclr_last.pth`:
```bash
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml \
--work-dir runs/simclr \
--set method=simclr simclr.epochs=40 loader.num_workers=8 \
simclr.resume=runs/simclr/simclr_last.pth
```
日志会打印"已载入训练状态 …:从 epoch N 接着练到 40"。恢复的不只是权重,还有**投影头、
优化器动量、学习率调度进度和 step 计数**——少了这些,续训等于把动量和 cosine 进度清零,
损失会先反弹一段。
> `resume` 与 `checkpoint` 语义不同,二选一(同时给会报错):
> **`resume`** = 载入完整状态**接着训练**(要 `simclr_last.pth`);
> **`checkpoint`** = 有权重就**跳过训练**直接聚类(要 `simclr_backbone.pth`)。
> 拿 `simclr_backbone.pth` 去 `resume` 会报错,因为它不含投影头和优化器状态。
#### 第二步:复用权重重新聚类
想换簇数或改聚类参数时**不必重训**——把权重路径填进 `simclr.checkpoint` 就会跳过训练
(日志会打印"复用已有的自监督骨架"),这一步只跑特征提取 + 聚类,约 **1 分钟**:
```bash
# 换成 k=2(实测 SimCLR 特征在 k=2 上最干净:轮廓 0.469 / 稳定性 0.991)
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml \
--work-dir runs/simclr_k2 \
--set method=simclr simclr.checkpoint=runs/simclr/simclr_backbone.pth \
--num-clusters 2
# 先扫一遍簇数再决定(只打指标表,不产出分组)
uv run python scripts/cluster_unlabeled.py --config configs/cluster/film.yaml \
--set method=simclr simclr.checkpoint=runs/simclr/simclr_backbone.pth \
--sweep-k 2-8
```
> 复用权重时给一个**新的 `--work-dir`**,否则新的聚类产出会覆盖同目录下的旧结果。
**实测效果**(14,579 张,与 pretrained 用完全相同的聚类设置):只训 **1 个 epoch** 的域内特征
在 k=4 上就追平 ImageNet(轮廓 0.334 vs 0.325),在 **k=2 上大幅胜出**(轮廓 0.469、
稳定性 0.991)——目视确认这个二分正是"有明显缺陷 / 弱纹理近空白",可直接当作流水线的
第一级过滤。详见 [文档 §10.5](docs/unsupervised-clustering.md#105-simclr-域内自监督-vs-imagenet-预训练)。
权重存到 `<work_dir>/simclr_backbone.pth`(**只含骨架,不含投影头**——投影头被训练成
对增强不变,会丢掉下游有用的信息,所以聚类和保存都只用骨架特征)。
#### 固化到配置
每次敲 `--set` 麻烦的话,直接改 [configs/cluster/film.yaml](configs/cluster/film.yaml):
```yaml
method: simclr
simclr:
# 自监督用项目自己的纯卷积骨架(与 method=pretrained 的顶层 `backbone` 各自独立,
# 两者字段完全不同,分开配置以免 _base_ 合并时串味)
backbone: {name: tiny_conv, in_channels: 3, widths: [32, 64, 128, 256], depths: [1, 2, 2, 2]}
epochs: 40
batch_size: 256
temperature: 0.2 # 越小越关注难负样本
amp: auto # CUDA 上开、MPS 上关
save_interval: 5 # 每 5 个 epoch 落一次盘
# 下面两个二选一,语义不同:
checkpoint: null # 有权重就跳过训练直接聚类,例 runs/simclr/simclr_backbone.pth
resume: null # 载入完整状态接着训练,例 runs/simclr/simclr_last.pth
projection: {hidden_channels: 256, out_channels: 128}
optimizer: {name: adamw, lr: 0.001, weight_decay: 0.0001}
scheduler: {name: cosine}
view_transforms: # 生成两个视图的随机增强
- {name: RandomResizedCrop, size: [128, 128], scale: [0.5, 1.0]}
- {name: RandomHorizontalFlip, p: 0.5}
- {name: RandomVerticalFlip, p: 0.5}
- {name: GaussianBlur, p: 0.3}
```
> `RandomResizedCrop` 的 `scale` 下限别低于 0.4:裁太狠会让两个视图都只剩背景,
> "背景 vs 背景"被当成正样本对,学出来的特征会退化。
SimCLR 训练同样接了 SwanLab(配置在 `simclr.swanlab`,字段与 `train.swanlab` 相同,
默认 `mode: local` 不需要登录)。训练跑起来后另开一个终端:
```bash
uv run swanlab watch runs/simclr/swanlog
```
记录的指标:按 step 的 `simclr/loss`、`simclr/loss_avg`、`simclr/lr`,按 epoch 的
`simclr/epoch_loss`、`simclr/epoch_seconds`、`simclr/gpu_mem_peak_mb`,超参是完整的 yaml 配置。
不想要就 `--set simclr.swanlab.enabled=false`;纯看文本日志用
`grep "完成 loss" runs/simclr/cluster.log`。
训练出的骨架和检测模型用的是同一套卷积骨架,可以直接拿去初始化 `DefectNet` 的 backbone——
标注量有限时,通常比 ImageNet 权重更贴合薄膜的成像特性。
## 方法
- **骨架**:`tiny_conv`(自研深度可分离卷积,默认)或 `torchvision`(resnet / mobilenet / shufflenet)。
- **neck**:nearest 上采样 + 3×3 卷积,把特征恢复到 stride 4,兼顾小缺陷定位与速度。
- **定位头**:anchor-free 中心点(CenterNet 风格)——`heatmap` + `wh` + `offset`,
推理端用 3×3 max-pool 做峰值抑制,无 NMS 循环,便于导出到 TensorRT / OpenVINO。
- **分类头**:全局池化 + 全连接,输出**多热**标签(一张图可同时含多种缺陷,全 0 表示 OK 片)。
- **指标**:定位 precision / recall / F1 / mean IoU(IoU 阈值匹配),分类 micro-F1 与整图完全正确率。
`train.monitor` 决定用哪个指标挑选 `best.pth`。
配置支持 `_base_` 继承与命令行 `--set a.b.c=value` 覆盖,见 [utils/config.py](utils/config.py)。
新增骨架 / 数据集只需用注册表装饰器注册,再在 yaml 里按 `name` 引用。
## 开发
```bash
uv run pytest # 单元测试
uv run ruff check . # 静态检查
```