Spaces:
Running on Zero
Running on Zero
File size: 4,374 Bytes
4b98524 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | """Check a labelling run against the gate before anyone trains on it.
.venv/bin/python -m app.training.validate cattle_weight data/weight.jsonl
.venv/bin/python -m app.training.validate cattle_bcs data/bcs.jsonl
What this can tell you is whether the **data** could support a model that
passes: enough ground truth, enough farms, enough breeds, and coverage across
the range rather than a pile in the middle. What it cannot tell you is whether a
model trained on it hits the metric β that needs the model. Exit code 0 means
"worth training on", never "ready to promote".
Run it while collection is still happening. A dataset that turns out to be four
hundred animals from one farm is recoverable in month two and not in month six.
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from pydantic import ValidationError
from app.training.gates import PROMOTION_GATES, PromotionGate
from app.training.schema import BcsSample, WeightSample
_MODELS = {"cattle_weight": WeightSample, "cattle_bcs": BcsSample}
def _bucket(sample) -> str:
if isinstance(sample, WeightSample):
return sample.weight_band
return f"bcs_{sample.consensus:.1f}"
def _load(path: Path, model) -> tuple[list, list[str]]:
samples, problems = [], []
for number, line in enumerate(path.read_text().splitlines(), start=1):
line = line.strip()
if not line or line.startswith("#"):
continue
try:
samples.append(model.model_validate(json.loads(line)))
except (json.JSONDecodeError, ValidationError) as exc:
first = str(exc).splitlines()[-1].strip()
problems.append(f"line {number}: {first}")
return samples, problems
def report(samples: list, gate: PromotionGate) -> list[tuple[bool, str]]:
truth = [s for s in samples if s.is_ground_truth]
farms = {s.farm_id for s in truth}
animals = {s.animal_id for s in truth}
breeds = {s.breed for s in truth}
buckets = Counter(_bucket(s) for s in truth)
total = len(truth) or 1
thin = sorted(
b for b, n in buckets.items() if n / total < gate.min_share_per_bucket
)
lines = [
(len(truth) >= gate.min_ground_truth_samples,
f"ground-truth samples: {len(truth)} of {gate.min_ground_truth_samples} "
f"({len(samples) - len(truth)} excluded as estimates or unusable)"),
(len(animals) >= gate.min_animals,
f"distinct animals: {len(animals)} of {gate.min_animals}"),
(len(farms) >= gate.min_farms,
f"farms: {len(farms)} of {gate.min_farms}"),
(len(breeds) >= gate.min_breeds,
f"breeds: {len(breeds)} of {gate.min_breeds} β {', '.join(sorted(breeds)) or 'none'}"),
(not thin,
f"buckets under {gate.min_share_per_bucket:.0%}: "
f"{', '.join(thin) if thin else 'none'}"),
(len(farms) >= gate.min_farms + 2,
f"farms to spare for the held-out split: {max(len(farms) - gate.min_farms, 0)} of 2"),
]
return lines
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("capability", choices=sorted(_MODELS))
parser.add_argument("manifest", type=Path)
args = parser.parse_args(argv)
gate = PROMOTION_GATES[args.capability]
samples, problems = _load(args.manifest, _MODELS[args.capability])
print(f"{args.manifest} β {args.capability}")
if problems:
print(f"\n {len(problems)} record(s) rejected:")
for problem in problems[:20]:
print(f" β {problem}")
if len(problems) > 20:
print(f" β¦ and {len(problems) - 20} more")
print()
lines = report(samples, gate)
for passed, text in lines:
print(f" {'β' if passed else 'β'} {text}")
print(f"\n held-out split: {gate.holdout}")
print(f" metric to beat: {gate.metric}")
print(f" {gate.threshold}")
ready = all(passed for passed, _ in lines) and not problems
verdict = (
"The data could support a model that passes. Train, then measure."
if ready
else "Not enough data yet. Keep collecting."
)
print(f"\n {verdict}")
return 0 if ready else 1
if __name__ == "__main__":
sys.exit(main())
|