filter-inspection / docs /REPRODUCE.md
daipath's picture
加入完整文档集:复现指南、逐文件说明、结果汇总、诚实性审计、已知边界
0e9808f verified
|
Raw
History Blame Contribute Delete
15.4 kB
# 完整复现指南
从零复现本项目的全部实验。按顺序执行即可。
**总耗时**:数据准备约 20 分钟,全部训练约 14 小时(单张 RTX 4090)。
只想复现分类主线结论:约 3 小时。
---
## 0. 硬件与环境
实测环境(结果就是在这上面跑出来的):
| | |
|---|---|
| GPU | NVIDIA RTX 4090 24 GB |
| CUDA | 12.8 |
| Python | 3.12.3 |
| OS | Linux 6.8(Ubuntu 24.04 容器) |
### 建环境
```bash
python3.12 -m venv /workspace/venvs/seg
source /workspace/venvs/seg/bin/activate
pip install -U pip
pip install torch==2.11.0 torchvision --index-url https://download.pytorch.org/whl/cu128
pip install timm==1.0.28 segmentation-models-pytorch==0.5.0 albumentations==2.0.8 \
opencv-python-headless==5.0.0 numpy==2.4.4 pandas==3.0.5 \
scipy scikit-learn pillow openpyxl tqdm huggingface_hub
```
版本敏感度:`albumentations` 2.x 的 `PadIfNeeded(fill=…)` 参数名与 1.x 不同(1.x 是 `value=`),
脚本按 2.x 写。`timm` 需 ≥1.0 才有 `edgenext_small.usi_in1k` / `efficientvit_b1.r288_in1k`
```bash
export HF_HOME=/workspace/hf_cache # timm 预训练权重缓存,避免重复下载
export HF_HUB_DISABLE_XET=1 # 本容器里不加这条 HF 上传/下载会报 httpx 错
```
---
## 1. 拿数据
```bash
cd /workspace/CV_Task/seg3/newdata
hf download daipath/filter-inspection-data --repo-type dataset --local-dir ./dl
```
如果你要从**最原始的压缩包**开始(验证解包逻辑):
```bash
bash fetch_and_unpack.sh # 下 样本7_27.zip 并递归解包到 raw/
```
`unpack.py` 会做两件不显然的事:
1. **修 GBK 文件名**。zip 里中文名以 cp437 存储,按 `cp437 → gbk → gb18030 → big5` 顺序回退解码。不修的话目录名全是乱码,`pkg` 字段就废了。
2. **防 zip-slip**。拒绝解到 `raw/` 之外的路径。
解包后应得到 **3728 张 PNG + 1977 个 labelme JSON**,分布在 11 个包目录下。
---
## 2. 清点与几何量测
```bash
python inventory.py # -> inventory.csv 3728 行
python metrics_fix.py # -> metrics_fixed.csv 1948 行(仅已标注图)
```
### `inventory.py` 里两条必须原样保留的正则
```python
RE_BATCH = re.compile(r'(?<![0-9])(38[0-9]{3})(?![0-9])')
RE_PMACH = re.compile(r'(?<![A-Za-z0-9])[Pp]([0-9]{2})([0-9]{6})?(?![0-9])')
```
**没有批次号 / 机器号的包,字段留空,不做任何默认填充。**
11 个包里有 5 个没有机器号——如果你给它们编一个默认值,`group` 分组就会把不同来源的图混成一组,切分立刻泄漏。
`FINE_VERT_THR = 30`:标注顶点数中位 ≥30 判为「精细描边」,否则「粗框」。
这条把 11 个包分成 labelme 3.3.10(顶点中位 80,精描)和 5.11.4(顶点中位 12,粗框)两档。
`样本_38031` **两个版本混装**,要按版本拆开再用。
### `metrics_fix.py` 修掉的两个 bug
原版 `metrics.py` 有两处错:
1. **circle 类型的 shape 被漏计**。labelme 的 `circle` 只存圆心+半径两点,原版按多边形算面积得 0。修法是展开成 24 边形再算。
2. **`tightness` 指标是错的**——它拿多边形面积除以外接框面积,但崩边缺陷的多边形大部分画在片体外,这个比值没有物理意义。换成两个真实可解释的量:
- `in_body_ratio`:多边形落在片体内的面积占比
- `vert_density`:顶点数 / 周长,用来区分精描与粗框
---
## 3. 切分
```bash
python build_splits.py # -> splits/splitA/{meta,dropped}.csv
# splits/splitB/{meta,dropped}.csv
```
### 两套切分
| | A 套(全混合) | B 套(跨批次) |
|---|---|---|
| 目的 | 常规泛化 | **换批次还灵不灵** |
| 做法 | 11 个包混合,8:1:1 分层 | `样本_38016_P08262014` **整包**当 test |
| train / val / test | 2768 / 434 / 409 | 2787 / 441 / 284 |
| test 良品 / 不良 | 185 / 224 | 185 / 99 |
两套都从 3728 张里**剔除 117 张**
| 原因 | 张数 | 说明 |
|---|---|---|
| `rare_class` | 83 | 「碎」64 + 「不可擦脏」19,整类排除 |
| `zero_shapes` | 28 | 有 json 但 `shapes` 为空数组,零监督 |
| `no_json` | 6 | 缺标注文件 |
剩 3611 张。剔除清单在 `dropped.csv`,每行带原因,可复查。
> 「碎」虽然被排除,但它在 [审计](AUDIT.md) 里当**未见类硬正样本**用——模型从没见过这类,能不能抓到是诚实性的关键证据。
### 分组规则(最重要的一条)
分组单位是 **`group = pkg ‖ sess`**(包 ‖ 拍摄会话),**同一组的图绝不跨 split**
288 个组,两套切分都实测 **0 组跨 split**。验证:
```bash
python -c "
import pandas as pd
for s in ('A','B'):
d = pd.read_csv(f'splits/split{s}/meta.csv')
bad = d.groupby('group').split.nunique()
print(f'split{s}: {len(bad)} 组, 跨split的 {(bad>1).sum()} 组')
"
# 期望:splitA: 288 组, 跨split的 0 组 / splitB 同
```
**不遵守这条,跑出来的数字会虚高。** 同一次拍摄的图之间相关性极强,随机分会让 val/test 里塞满 train 的近邻。
---
## 4. 分割路线(历史主线,结论是「不如分类」)
```bash
python train_v3.py splits/splitA 200 384 32 10 > splits/splitA/train.log 2>&1
python train_v3.py splits/splitB 200 384 32 10 > splits/splitB/train.log 2>&1
```
约 3.5 小时 / 套。参数是 `<split目录> <epochs> <size> <batch> <workers>`
`train_v3.py` 相对 seg3 初版的改动,以及**为什么**
| 改动 | 原因 |
|---|---|
| 全量 RAM 预载 | 图只有 300 px,3611 张全进内存约 1.2 GB,把 dataloader 从瓶颈里拿掉 |
| batch 32 + `channels_last` + bf16 | 吃满 4090;显存占用约 18 GB |
| 归一化换成灰世界 + z-score(`gwz`) | 数据存在采集配置色调漂移(B 通道逐日 15→8→5),ImageNet 固定均值方差顶不住 |
| 固定随机种子 42 | 可复现 |
| **加了 test 泄漏断言** | 见下 |
```python
te_n = int((df.split == 'test').sum())
assert 'test' not in set(tr_df.split) | set(va_df.split), 'test 泄漏进了训练/选型集'
```
**这条断言不要删。** 本项目上一代就栽在切分泄漏上,加断言比事后审计便宜得多。
### 评估
```bash
python eval_v3.py splits/splitA # -> eval_report.json, eval_{val,test}.csv, curve_{val,test}.csv
python eval_v3.py splits/splitB
```
预期(`eval_report.json`):
| | A 套 val | A 套 test | B 套 val | B 套 test |
|---|---|---|---|---|
| 三类合计 Dice | 0.6975 | 0.7432 | 0.7021 | 0.6802 |
| 划痕 Dice | 0.6366 | 0.6578 | 0.6502 | 0.5659 |
| 崩边 Dice | 0.7713 | 0.8489 | 0.7652 | 0.7811 |
| **麻点 Dice** | **0.4580** | **0.3728** | **0.4403** | **0.2609** |
| AUROC (`s_max`) | 0.9779 | 0.9922 | 0.9884 | 0.9749 |
麻点 Dice 只有 0.26–0.46,是全局短板。`train_summary.json` 里 A 套最佳 epoch = 146 / 200,
最后 20 epoch 的 val Dice 标准差 0.0047 —— **已收敛,不需要早停**
### 增广消融
```bash
bash run_aug_ablation.sh 200 # 4 档 × 2 切分 = 8 个作业
```
A 套结果(`splits/splitA/aug_compare.json`),`rec1`/`fp1` 是单阈值口径:
| 增广 | 平均 Dice | 单阈值检出 | 误报 |
|---|---|---|---|
| none | 0.5947 | 65.6% | 2 |
| **weak** | **0.6136** | **70.1%** | 2 |
| mid | 0.6061 | 59.4% | 2 |
| strong | 0.6086 | 56.7% | 3 |
**weak 最好,strong 反而更差。** 缺陷本身就是细微低对比结构,强增广会把它抹掉。
### 判据优化(几何规则)
```bash
python measure_geom.py splits/splitA # -> geom_{val,test}.csv 逐图几何量
python optimize_rules.py # -> rules_maxfp3.json, rules3_maxfp3.json
```
`rules_maxfp3.json` 里区分 **oracle**(阈值直接在 test 上调,作弊)和 **honest**(阈值在 val 上定):
| | A 套 honest | A 套 oracle | B 套 honest | B 套 oracle |
|---|---|---|---|---|
| 单一像素阈值 | 71.4% (fp 5) | 49.6% (fp 3) | 69.7% (fp 5) | 65.7% (fp 3) |
| 7 维几何判据 | 64.3% (fp 7) | 74.6% (fp 3) | 76.8% (fp 7) | 85.9% (fp 3) |
7 维是:划痕长度 L / 划痕面积 A / 麻点最大颗 P / 麻点直径 D / 崩边面积 C / 沿边弧长 E / 过角+面积 Cc。
**注意 honest 与 oracle 的落差**:A 套几何判据 oracle 74.6% → honest 64.3%,掉 10 个点。
7 个自由度在 434 张 val 上调,过拟合是必然的。**这就是几何判据路线的天花板。**
---
## 5. 分类路线(推荐主线)
### 第一阶段:骨干 × 增广 × 任务
```bash
EP=60 SZ=320 BS=32 CONC=2 bash run_cls_sweep.sh 1 # 选骨干
EP=60 SZ=320 BS=32 CONC=2 bash run_cls_sweep.sh 2 # 扫增广 × 任务 × 切分
```
产出 18 个模型到 `splits/split{A,B}/cls/`。命名格式 `{task}_{backbone}_{aug}_{size}`
- `task``bin`(良/不良)或 `cls4`(良/划/麻/崩)
- `aug``none` / `weak` / `mid` / `strong`
### 第二阶段:多轴消融
```bash
bash run_cls2.sh # 19 个作业,并发 3,约 4 小时
```
`cls_train2.py` 支持 6 个正交轴:
| 轴 | 取值 | 设计意图 |
|---|---|---|
| `--sz` | 224 / 256 / 320 / 384 / 448 | 麻点在 224 px 下只有 109 px²,320 下 223 px²,448 下 437 px² |
| `--norm` | `gwz` / `imnet` / `zscore` | **`imnet` 是 seg3 初版用的,怀疑它是最大隐患** |
| `--aug` | `geo` / `photo` / `mid` / `mixup` / `cutmix` | 把几何增广与光度增广拆开单测 |
| `--loss` | `ce` / `ce_nols` / `focal` | 标签平滑的影响、类别不均衡 |
| `--bb` | resnet18 / regnety_016 / repvgg_a1 / edgenext_small / convnext_tiny(DINOv3) / efficientvit_b1 | 覆盖 CNN / ViT / 自监督 / 部署友好 |
| `--task` | `bin` / `cls4` | |
### 统一评测
```bash
python cls_eval_all.py # -> cls_test_all.csv,42 个模型
python cls_tta_ens.py splits/splitA # -> tta_ens.json,TTA×8
```
`cls_eval_all.py` 的 `rec@K` / `fp@K` 定义要看清楚:
> **在 val 上搜一个阈值,使 val 误报 ≤ K;把这个阈值原样搬到 test。**
> `rec@K` 是 test 检出率,`fp@K` 是 **test 上实际发生的误报数**(不受 K 约束,可能超)。
这是诚实口径。CSV 里还有 `imp_test`(test 口径不良桶良品率)和 `imp_line10`(换算到产线不良率 10%)。
预期 top 结果(`cls_test_all.csv`):
| split | task | backbone | aug/norm | val AUROC | test AUROC | rec@3 | test fp |
|---|---|---|---|---|---|---|---|
| A | bin | efficientnet_b0 | mid/**imnet** | 0.9951 | **0.9976** | 94.6% | 3 |
| A | bin | efficientnet_b0 | cutmix/gwz | 0.9922 | 0.9975 | 95.5% | 4 |
| A | bin | mobilenetv3_large_100 | weak/gwz | 0.9917 | 0.9969 | 94.6% | 3 |
| A | bin | efficientnet_b0 | mid/gwz | 0.9936 | 0.9918 | **97.3%** | 4 |
| B | bin | resnet34 | mid/gwz | 0.9932 | 0.9862 | 94.9% | 7 |
**两个反直觉的点,都要照实记:**
1. **`gwz` 没有优势。** 最高 test AUROC 是 `imnet`(0.9976)拿的。我此前推测 `gwz` 能扛色调漂移——**在 test 上没验证出来**,此结论已撤回。真要证明 `gwz` 的价值,得做色调漂移压力测试,本项目没做。
2. **`convnext_tiny.dinov3_lvd1689m` 彻底失败**(val AUROC 0.5132,test 0.5335 = 随机)。DINOv3 预训练权重在 60 epoch、4e-4 的配置下没收敛。不是说这个骨干不行,是这套超参不适配它。
TTA×8 最好成绩:`mobilenetv3_large_100 weak` **0.9969 → 0.9982**。
注意 TTA 并非总是有益:`mobilenetv3 none` 0.9959 → 0.9925(**掉了**)。
四分类逐类召回(`cls4_resnet34_mid_320`,A 套 test):
| GT 类别 | 张数 | 判对 | 召回 |
|---|---|---|---|
| 划痕 | 133 | 117 | 88.0% |
| 崩边 | 68 | 64 | 94.1% |
| 良品 | 185 | 176 | 95.1% |
| 麻点 | 23 | 21 | **91.3%** |
> `eval.json` 里 `confusion` 的存储方向是 **外层 key = 预测类,内层 key = 真实类**。
> 读反了会得到完全不同的数字(比如把 88.0% 读成 94.4%)。
---
## 6. 诚实性审计(**不要跳过**)
```bash
python cls_probe.py # -> cls_probe_all.csv
python cls_frozen_probe.py splits/splitA resnet34 320 # -> frozen_probe_resnet34_320.json
```
四道审计的原理、预期数字和判读方式见 [`AUDIT.md`](AUDIT.md)。
一句话版本:**图像里确实带着可区分批次的信息(包分类准确率 92.7%),但模型没去用它**——
只见良品的 kNN(0.9634)反而**打败**了用了标签的线性探针(0.9461),说明标签解锁不了额外的捷径。
---
## 7. 导出与打包部署
```bash
python export_onnx.py # -> release/filter_binary.onnx + .json
# release/filter_4class.onnx + .json
```
**`export_onnx.py` 里 `dynamo=False` 不能删。** 用新的 dynamo 导出器会把权重拆成独立的
`.onnx.data` 文件,单文件分发就没了。
打包成免安装可执行文件:
```bash
R=/workspace/CV_Task/seg3/newdata/release
pyinstaller --onefile --name filter-predict-cpu \
--add-data "$R/filter_binary.onnx:." --add-data "$R/filter_binary.json:." \
--add-data "$R/filter_4class.onnx:." --add-data "$R/filter_4class.json:." \
--add-binary /workspace/miniconda3/lib/libstdc++.so.6:. \
"$R/predict.py"
```
**三个踩过的坑,照抄即可避免:**
1. `--add-data` 的相对路径是**相对 specpath 解析**的,不是相对 cwd。用绝对路径 `$R/...`
2. 打包后 `__file__` 指向 `/tmp/_MEIxxx`,模型找不到。`predict.py` 里的 `sys.frozen` / `_MEIPASS` 分支处理这个,别简化掉。
3. **`libstdc++.so.6` 必须显式打进去**。Ubuntu 24.04 的系统库要 GLIBC_2.38,目标机上没有就直接崩。
miniconda 那份只要 glibc 2.26。全部 141 个打包 .so 审计下来最高需求 glibc 2.28。
> 这个错的表象是 numpy 导入失败,**根因不是 numpy**。曾按 numpy 方向排查很久,是弯路。
GPU 版把 `onnxruntime` 换成 `onnxruntime-gpu`,且**必须钉版本**
1.28 要 CUDA 13,目标机是 CUDA 12.8 —— 用 `1.22.0`(py ≤ 3.12)或 `1.24.4`(py 3.14)。
---
## 8. 生成报告
```bash
python gen_report.py # 数据核查报告
python gen_showcase.py # 分割效果展示
python gen_nonzero_goods.py # 被模型报警的良品
python gen_p10_report.py # 产线不良率 10% 判废纯度
python precision_table.py # 判废纯度对照表
```
5 份 HTML 的内容说明见 [`FILES.md`](FILES.md)。
---
## 复现检查清单
跑完对一下这几个数,对不上说明哪一步偏了:
- [ ] `inventory.csv` **3728** 行
- [ ] `splits/splitA/meta.csv` **3611** 行,`dropped.csv` **117** 行
- [ ] 跨 split 的组 **0** 个(288 组)
- [ ] 分割 A 套 test 三类合计 Dice **≈0.743**,麻点 Dice **≈0.373**
- [ ] 分类 A 套 test AUROC 最高 **≈0.9976**(efficientnet_b0 / mid / imnet)
- [ ] 未见类「碎」检出 `sui` **≥0.92**
- [ ] 冻结特征 kNN test AUROC **≈0.9634** > 线性探针 **≈0.9461**
随机性说明:训练固定了种子 42,但 cuDNN 非确定性算子仍会带来
**±0.005 量级的 AUROC 抖动**。这个噪声地板和榜单前 10 名之间的差距是同量级的
——**不要根据 0.001 的差异下结论**。要压过噪声需要多种子重复,本项目没做。