refactor: 按实际的五步工作流裁掉无关代码
实际工作流是:simclr 训练 -> 聚类 -> 人工归类 -> 分类训练 -> GradCAM/SHAP 生成
热力图与 box。据此移除不参与其中的检测/定位链路,共 31 个文件、约 2600 行:
- 模型:defect_net、postprocess、necks/、heads/center_head
- 数据:film_defect(YOLO 读取端)、target_encoder
- 引擎:trainer、evaluator、losses
- 入口:train.py、eval.py
- 脚本:convert_annotations、split_dataset、export_onnx、profile_inference
- 配置:configs/{train,eval,model,dataset}
- utils/labels(多类/单类标签模式,仅检测分类头使用)
共享模块按实际用量瘦身:engine/metrics 只留 AverageMeter(检测指标随之移除);
datasets/yolo 只留写出侧(IMAGE_SUFFIXES 与 write_data_yaml)——第 5 步产出的
YOLO 标签是给外部检测训练用的交换格式,读取端不再需要。
裁剪过程中修掉一个真 bug:generate_boxes 的 --stage 默认 6(layer3)对 tiny_conv
这类非 nn.Sequential 骨架直接抛异常。现在自动退回骨架最终输出并警告,
另加 --stage -1 显式选择最终输出。
五步工作流已端到端实跑验证(simclr 1 轮 -> 聚类 -> 导出 -> 分类 1 轮 ->
gradcam 与 shap 两种方法各生成一遍)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
57446c2f10
commit
86c86b105c
@@ -94,21 +94,25 @@ def test_both_methods_produce_a_yolo_dataset(project, tmp_path, monkeypatch, met
|
||||
assert all(0.0 <= float(v) <= 1.0 for v in parts[1:])
|
||||
|
||||
|
||||
def test_output_is_readable_by_the_detection_dataset(project, tmp_path, monkeypatch):
|
||||
"""产出要能直接喂给检测训练——这是这一步存在的意义。"""
|
||||
from datasets import FilmDefectDataset
|
||||
def test_output_is_a_wellformed_yolo_dataset(project, tmp_path, monkeypatch):
|
||||
"""产出是给外部检测训练用的交换格式,必须自洽:
|
||||
data.yaml 的类别与 labels/ 对得上,每行标签可解析且坐标在 [0,1] 内。"""
|
||||
import yaml
|
||||
|
||||
out = tmp_path / "out"
|
||||
run(base_argv(project, out, "gradcam"), monkeypatch)
|
||||
|
||||
dataset = FilmDefectDataset(
|
||||
img_dir=out / "images", classes=str(out / "data.yaml"),
|
||||
transforms=[{"name": "Resize", "size": [64, 64]}, {"name": "ToTensor"}], stride=4,
|
||||
)
|
||||
assert len(dataset) == 5
|
||||
sample = dataset[0]
|
||||
assert sample["image"].shape == (3, 64, 64)
|
||||
assert sample["boxes"].shape[1] == 4
|
||||
meta = yaml.safe_load((out / "data.yaml").read_text(encoding="utf-8"))
|
||||
names = [meta["names"][i] for i in sorted(meta["names"])]
|
||||
assert meta["nc"] == len(names) == 2
|
||||
|
||||
for label in (out / "labels").rglob("*.txt"):
|
||||
assert (out / "images" / label.parent.name / f"{label.stem}.png").is_file()
|
||||
for line in label.read_text(encoding="utf-8").strip().splitlines():
|
||||
class_id, cx, cy, w, h = line.split()
|
||||
assert 0 <= int(class_id) < meta["nc"]
|
||||
assert all(0.0 <= float(v) <= 1.0 for v in (cx, cy, w, h))
|
||||
assert float(w) > 0 and float(h) > 0
|
||||
|
||||
|
||||
def test_images_are_hard_links_not_copies(project, tmp_path, monkeypatch):
|
||||
@@ -224,3 +228,59 @@ def test_duplicate_source_names_are_rejected(tmp_path):
|
||||
gb._emit(source=Path("/data/dirA/x.png"), **common)
|
||||
with pytest.raises(gb.NameCollision, match="prefix-parent"):
|
||||
gb._emit(source=Path("/data/dirB/x.png"), **common) # 同名,撞同一个标签路径
|
||||
|
||||
|
||||
def test_gradcam_falls_back_when_backbone_has_no_sequential_features(tmp_path, monkeypatch):
|
||||
"""tiny_conv 不是 nn.Sequential,取不了中间层——应当退回最终输出并警告,而不是崩掉。"""
|
||||
from engine.checkpoint import save_checkpoint
|
||||
from models import build_model
|
||||
|
||||
root = tmp_path / "dataset"
|
||||
(root / "a").mkdir(parents=True)
|
||||
rng = np.random.default_rng(0)
|
||||
for i in range(2):
|
||||
Image.fromarray(rng.integers(0, 255, (40, 56, 3), dtype=np.uint8)).save(
|
||||
root / "a" / f"a_{i}.png"
|
||||
)
|
||||
|
||||
backbone = {"name": "tiny_conv", "widths": [8, 16], "depths": [1, 1]}
|
||||
checkpoint = tmp_path / "best.pth"
|
||||
save_checkpoint(checkpoint, build_model(
|
||||
{"name": "classifier", "num_classes": 1, "backbone": dict(backbone)}
|
||||
))
|
||||
|
||||
config = tmp_path / "cfg.yaml"
|
||||
config.write_text(
|
||||
f"""
|
||||
device: cpu
|
||||
data:
|
||||
root: {root}
|
||||
classes: null
|
||||
exclude: []
|
||||
val_transforms: [{{name: Resize, size: [64, 64]}}, {{name: ToTensor}}]
|
||||
loader: {{batch_size: 2, num_workers: 0}}
|
||||
model:
|
||||
name: classifier
|
||||
backbone: {{name: tiny_conv, widths: [8, 16], depths: [1, 1]}}
|
||||
init_from: null
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
out = tmp_path / "out"
|
||||
monkeypatch.setattr(
|
||||
sys, "argv",
|
||||
["generate_boxes.py", "--out", str(out), "--method", "gradcam",
|
||||
"--checkpoint", str(checkpoint), "--config", str(config), "--no-heatmaps"],
|
||||
)
|
||||
assert gb.main() == 0 # 不崩
|
||||
|
||||
summary = json.loads((out / "summary.json").read_text("utf-8"))
|
||||
assert summary["stage"] is None # 已退回最终输出
|
||||
assert len(list((out / "labels").rglob("*.txt"))) == 2
|
||||
|
||||
|
||||
def test_stage_minus_one_selects_the_final_output(project, tmp_path, monkeypatch):
|
||||
out = tmp_path / "out"
|
||||
assert run(base_argv(project, out, "gradcam") + ["--stage", "-1"], monkeypatch) == 0
|
||||
assert json.loads((out / "summary.json").read_text("utf-8"))["stage"] is None
|
||||
|
||||
Reference in New Issue
Block a user