# 完整复现指南 从零复现本项目的全部实验。按顺序执行即可。 **总耗时**:数据准备约 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'(? 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 小时 / 套。参数是 ` `。 `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 的差异下结论**。要压过噪声需要多种子重复,本项目没做。