Support governed train-only corpus shards
This commit is contained in:
@@ -142,6 +142,15 @@ def test_manifest_sample_selection_keeps_external_holdouts_out_of_targeted_datas
|
|||||||
module.select_manifest_samples(samples, {"geel", "missing"})
|
module.select_manifest_samples(samples, {"geel", "missing"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_train_only_shard_requires_explicit_opt_in() -> None:
|
||||||
|
module = load_tile_exporter()
|
||||||
|
samples = [{"sample_slug": "new-train-aoi", "recommended_split": "train"}]
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="must include at least one"):
|
||||||
|
module.validate_validation_split(samples, set())
|
||||||
|
assert module.validate_validation_split(samples, set(), allow_empty=True) == set()
|
||||||
|
|
||||||
|
|
||||||
def test_validation_coverage_reports_holdouts_without_retained_tiles() -> None:
|
def test_validation_coverage_reports_holdouts_without_retained_tiles() -> None:
|
||||||
module = load_tile_exporter()
|
module = load_tile_exporter()
|
||||||
coverage = module.validation_sample_coverage(
|
coverage = module.validation_sample_coverage(
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ def parse_args() -> argparse.Namespace:
|
|||||||
"Defaults to all documented validation samples."
|
"Defaults to all documented validation samples."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--allow-empty-validation",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Allow a train-only export shard with no validation AOI. This is only for later "
|
||||||
|
"composition into a dataset whose independent validation split is supplied separately."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--negative-keep-ratio",
|
"--negative-keep-ratio",
|
||||||
type=float,
|
type=float,
|
||||||
@@ -210,13 +218,20 @@ def select_manifest_samples(
|
|||||||
return selected, excluded
|
return selected, excluded
|
||||||
|
|
||||||
|
|
||||||
def validate_validation_split(samples: list[dict[str, Any]], val_slugs: set[str]) -> set[str]:
|
def validate_validation_split(
|
||||||
|
samples: list[dict[str, Any]],
|
||||||
|
val_slugs: set[str],
|
||||||
|
*,
|
||||||
|
allow_empty: bool = False,
|
||||||
|
) -> set[str]:
|
||||||
sample_slugs = {
|
sample_slugs = {
|
||||||
str(sample.get("sample_slug") or "").strip().lower()
|
str(sample.get("sample_slug") or "").strip().lower()
|
||||||
for sample in samples
|
for sample in samples
|
||||||
if str(sample.get("sample_slug") or "").strip()
|
if str(sample.get("sample_slug") or "").strip()
|
||||||
}
|
}
|
||||||
if not val_slugs:
|
if not val_slugs:
|
||||||
|
if allow_empty:
|
||||||
|
return set()
|
||||||
raise SystemExit("YOLO validation split must include at least one sample")
|
raise SystemExit("YOLO validation split must include at least one sample")
|
||||||
unknown = val_slugs - sample_slugs
|
unknown = val_slugs - sample_slugs
|
||||||
if unknown:
|
if unknown:
|
||||||
@@ -625,7 +640,11 @@ def main() -> int:
|
|||||||
manifest_samples,
|
manifest_samples,
|
||||||
split_slugs(args.samples),
|
split_slugs(args.samples),
|
||||||
)
|
)
|
||||||
val_slugs = validate_validation_split(samples, split_slugs(args.val_samples))
|
val_slugs = validate_validation_split(
|
||||||
|
samples,
|
||||||
|
split_slugs(args.val_samples),
|
||||||
|
allow_empty=args.allow_empty_validation,
|
||||||
|
)
|
||||||
exported_tiles: list[dict[str, Any]] = []
|
exported_tiles: list[dict[str, Any]] = []
|
||||||
for sample in samples:
|
for sample in samples:
|
||||||
exported_tiles.extend(
|
exported_tiles.extend(
|
||||||
@@ -650,7 +669,7 @@ def main() -> int:
|
|||||||
kept_tiles = [tile for tile in exported_tiles if tile["kept"]]
|
kept_tiles = [tile for tile in exported_tiles if tile["kept"]]
|
||||||
if not any(tile["split"] == "train" for tile in kept_tiles):
|
if not any(tile["split"] == "train" for tile in kept_tiles):
|
||||||
raise SystemExit("YOLO tile dataset export produced no training tiles")
|
raise SystemExit("YOLO tile dataset export produced no training tiles")
|
||||||
if not any(tile["split"] == "val" for tile in kept_tiles):
|
if not args.allow_empty_validation and not any(tile["split"] == "val" for tile in kept_tiles):
|
||||||
raise SystemExit("YOLO tile dataset export produced no validation tiles")
|
raise SystemExit("YOLO tile dataset export produced no validation tiles")
|
||||||
dataset_yaml = write_dataset_yaml(args.output_dir, class_name)
|
dataset_yaml = write_dataset_yaml(args.output_dir, class_name)
|
||||||
positive_tiles = [tile for tile in kept_tiles if not tile["is_negative"]]
|
positive_tiles = [tile for tile in kept_tiles if not tile["is_negative"]]
|
||||||
@@ -684,6 +703,7 @@ def main() -> int:
|
|||||||
),
|
),
|
||||||
"excluded_sample_slugs": excluded_sample_slugs,
|
"excluded_sample_slugs": excluded_sample_slugs,
|
||||||
"validation_sample_slugs": sorted(val_slugs),
|
"validation_sample_slugs": sorted(val_slugs),
|
||||||
|
"train_only_shard": args.allow_empty_validation,
|
||||||
**validation_coverage,
|
**validation_coverage,
|
||||||
"tile_count": len(kept_tiles),
|
"tile_count": len(kept_tiles),
|
||||||
"positive_tile_count": len(positive_tiles),
|
"positive_tile_count": len(positive_tiles),
|
||||||
|
|||||||
Reference in New Issue
Block a user