From efd3272bdae96b1d383b90b28f1649873e1863a6 Mon Sep 17 00:00:00 2001 From: Jens Date: Mon, 27 Jul 2026 09:40:01 +0200 Subject: [PATCH] Drive corpus sampling from calibration failures --- .../test_failure_driven_yolo_sampling.py | 28 +++++++++++++++++++ scripts/build_failure_driven_yolo_sampling.py | 12 ++++++-- scripts/run_belgium_building_training_loop.py | 7 +++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_failure_driven_yolo_sampling.py b/backend/tests/test_failure_driven_yolo_sampling.py index 4afadfeb..ed8aeeff 100644 --- a/backend/tests/test_failure_driven_yolo_sampling.py +++ b/backend/tests/test_failure_driven_yolo_sampling.py @@ -52,3 +52,31 @@ def test_sampling_repeats_only_failed_region_train_tiles() -> None: assert not any("protected" in path for path in paths) assert metadata["protected_samples_in_training"] == [] assert metadata["weak_recall_regions"] == ["flanders"] + + +def test_sampling_can_use_calibration_before_test_is_opened() -> None: + manifest = {"samples": [{"sample_slug": "train-fl", "split": "train", "region": "flanders"}]} + summary = { + "tiles": [ + {"sample_slug": "train-fl", "split": "train", "label_count": 1, "image_path": "/tmp/fl.png"} + ] + } + assessment = { + "status": "continue_training_loop", + "gates": { + "min_region_f1": 0.45, + "min_region_precision": 0.5, + "min_region_recall": 0.4, + "max_pure_empty_false_positives": 0, + }, + "calibration": { + "regions": {"flanders": {"f1": 0.4, "precision": 0.6, "recall": 0.35}} + }, + "test": None, + "background": None, + } + paths, metadata = MODULE.build_sampling( + summary=summary, manifest=manifest, assessment=assessment + ) + assert len(paths) == 3 + assert metadata["failure_evidence_source"] == "calibration" diff --git a/scripts/build_failure_driven_yolo_sampling.py b/scripts/build_failure_driven_yolo_sampling.py index a9519342..85568071 100644 --- a/scripts/build_failure_driven_yolo_sampling.py +++ b/scripts/build_failure_driven_yolo_sampling.py @@ -34,7 +34,10 @@ def build_sampling( samples = {item["sample_slug"]: item for item in manifest["samples"]} gates = assessment["gates"] - regions = assessment["test"]["regions"] + evaluation = assessment.get("test") or assessment.get("calibration") + if not evaluation or "regions" not in evaluation: + raise ValueError("Assessment has no regional calibration or test evidence") + regions = evaluation["regions"] weak_recall_regions = { region for region, metrics in regions.items() @@ -46,8 +49,10 @@ def build_sampling( for region, metrics in regions.items() if metrics["precision"] < gates["min_region_precision"] } - background_failed = ( - assessment["background"]["pure_empty_false_positives"] + background = assessment.get("background") + background_failed = bool( + background + and background["pure_empty_false_positives"] > gates["max_pure_empty_false_positives"] ) @@ -77,6 +82,7 @@ def build_sampling( "schema_version": 1, "status": "ok", "strategy": "failed-region-positive-and-hard-negative-repeat", + "failure_evidence_source": "test" if assessment.get("test") else "calibration", "weak_recall_regions": sorted(weak_recall_regions), "weak_precision_regions": sorted(weak_precision_regions), "background_gate_failed": background_failed, diff --git a/scripts/run_belgium_building_training_loop.py b/scripts/run_belgium_building_training_loop.py index 433572da..11e8c8f6 100644 --- a/scripts/run_belgium_building_training_loop.py +++ b/scripts/run_belgium_building_training_loop.py @@ -262,6 +262,13 @@ def main() -> int: "phase": "calibration_rejected", "threshold_selection_source": "calibration_only", "selected_threshold": chosen["threshold"], + "gates": { + "min_aggregate_f1": args.min_aggregate_f1, + "min_region_f1": args.min_region_f1, + "min_region_precision": args.min_region_precision, + "min_region_recall": args.min_region_recall, + "max_pure_empty_false_positives": args.max_pure_empty_fp, + }, "calibration": chosen, "test": None, "background": None,