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
@@ -154,8 +154,9 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
parser.add_argument(
|
||||
"--stage", type=int, default=6,
|
||||
help="gradcam 的目标层(4~7 = layer1~4)。默认 6 即 layer3(8×8);"
|
||||
"layer4 只有 4×4 太粗,layer2 响应太散",
|
||||
help="gradcam 的目标层(torchvision 骨架:4~7 = layer1~4)。默认 6 即 layer3(8×8);"
|
||||
"layer4 只有 4×4 太粗,layer2 响应太散。-1 表示用骨架最终输出——"
|
||||
"tiny_conv 这类不是 nn.Sequential 的骨架只能这样取",
|
||||
)
|
||||
parser.add_argument("--max-evals", type=int, default=500, help="shap 每张图的前向次数")
|
||||
parser.add_argument("--smooth-radius", type=int, default=2, help="shap 归因图平滑半径,0=不平滑")
|
||||
@@ -211,6 +212,19 @@ def main() -> int:
|
||||
for param in model.parameters(): # 归因需要梯度,但不更新权重
|
||||
param.requires_grad_(True)
|
||||
|
||||
stage = args.stage if args.stage >= 0 else None
|
||||
if stage is not None and not isinstance(
|
||||
getattr(model.backbone, "features", None), torch.nn.Sequential
|
||||
):
|
||||
# tiny_conv 这类骨架没有 nn.Sequential 的 features,取不了中间层;
|
||||
# 退回最终输出而不是直接崩掉
|
||||
logger.warning(
|
||||
"骨架 %s 不是 nn.Sequential,无法按 stage 取中间层,改用最终输出"
|
||||
"(分辨率更低,如需更精细请换 torchvision 骨架)",
|
||||
type(model.backbone).__name__,
|
||||
)
|
||||
stage = None
|
||||
|
||||
min_area_ratio = (
|
||||
args.min_area_ratio if args.min_area_ratio is not None
|
||||
else DEFAULT_MIN_AREA_RATIO[args.method]
|
||||
@@ -253,7 +267,7 @@ def main() -> int:
|
||||
predicted = model(images).argmax(1)
|
||||
targets = predicted if args.target == "predicted" else labels
|
||||
agree += int((predicted == labels).sum())
|
||||
heats = grad_cam(model.backbone, model.head, images, targets, stage=args.stage)
|
||||
heats = grad_cam(model.backbone, model.head, images, targets, stage=stage)
|
||||
heat_shape = tuple(heats.shape[-2:])
|
||||
|
||||
for heat, target, meta in zip(
|
||||
@@ -310,7 +324,7 @@ def main() -> int:
|
||||
"heatmap_shape": list(heat_shape) if heat_shape else None,
|
||||
"seconds_per_image": (time.time() - started) / max(total, 1),
|
||||
"box_params": box_kwargs,
|
||||
"stage": args.stage if args.method == "gradcam" else None,
|
||||
"stage": stage if args.method == "gradcam" else None,
|
||||
"max_evals": args.max_evals if args.method == "shap" else None,
|
||||
"prediction_matches_folder_label": agree / total if total else 0.0,
|
||||
"boxes_per_image": {str(k): v for k, v in sorted(box_counts.items())},
|
||||
|
||||
Reference in New Issue
Block a user