Files
Shaun ZhouandClaude Opus 5 86c86b105c 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>
2026-09-11 10:27:20 +08:00

72 lines
2.7 KiB
Python

"""卷积骨架与分类模型。"""
import pytest
torch = pytest.importorskip("torch")
from models import build_model # noqa: E402
from models.backbones.resnet import _SUPPORTED as _SUPPORTED_ARCHS # noqa: E402
from models.heads.projection import ProjectionHead # noqa: E402
@pytest.mark.parametrize("arch", sorted(_SUPPORTED_ARCHS))
def test_every_supported_torchvision_arch_actually_works(arch):
"""清单里列出的每个骨架都必须真能前向,且声明的 out_channels 与实际一致。
回归测试:shufflenet_v2_x1_0 曾被列为支持,但它没有 `.features` 属性,
构造时直接 AttributeError。
"""
from models.registry import BACKBONES
backbone = BACKBONES.build({"name": "torchvision", "arch": arch, "pretrained": False}).eval()
with torch.no_grad():
feat = backbone(torch.randn(1, 3, 64, 64))
assert feat.dim() == 4
assert feat.shape[1] == backbone.out_channels == _SUPPORTED_ARCHS[arch]
assert backbone.out_stride == 32
def test_unsupported_arch_is_rejected():
from models.registry import BACKBONES
with pytest.raises(ValueError, match="不支持的骨架"):
BACKBONES.build({"name": "torchvision", "arch": "vit_b_16"})
def test_tiny_conv_shapes():
from models.registry import BACKBONES
backbone = BACKBONES.build(
{"name": "tiny_conv", "widths": [8, 16, 32], "depths": [1, 1, 1]}
).eval()
with torch.no_grad():
feat = backbone(torch.randn(2, 3, 64, 64))
assert feat.shape[:2] == (2, 32)
assert backbone.out_channels == 32
# stem 下采样 2 倍,其后每个 stage(除第一个)各下采样 2 倍
assert backbone.out_stride == 8
@pytest.mark.parametrize("arch", ["resnet18", "shufflenet_v2_x1_0", "mobilenet_v3_small"])
def test_alternative_backbones_drive_simclr(arch):
"""换骨架后 SimCLR 的投影头要能接上(out_channels 必须准)。"""
from models.registry import BACKBONES
backbone = BACKBONES.build({"name": "torchvision", "arch": arch, "pretrained": False}).eval()
head = ProjectionHead(backbone.out_channels, 32, 16)
with torch.no_grad():
assert head(backbone(torch.randn(2, 3, 64, 64))).shape == (2, 16)
def test_classifier_shapes_and_convolution_only():
model = build_model({
"name": "classifier", "num_classes": 4,
"backbone": {"name": "tiny_conv", "widths": [8, 16], "depths": [1, 1]},
"classification": {"dropout": 0.1},
}).eval()
with torch.no_grad():
assert model(torch.randn(3, 3, 64, 64)).shape == (3, 4)
forbidden = (torch.nn.MultiheadAttention, torch.nn.Transformer, torch.nn.LSTM)
assert not any(isinstance(m, forbidden) for m in model.modules())