131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""CUDA-train an isolated, non-promotable YOLO candidate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
EXPERIMENTAL_ROOT = Path("/app/storage/training/experimental")
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _inside(path: Path, root: Path) -> bool:
|
|
try:
|
|
path.resolve().relative_to(root.resolve())
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def validate_paths(dataset_dir: Path, output_dir: Path) -> dict:
|
|
if not _inside(dataset_dir, EXPERIMENTAL_ROOT) or not _inside(output_dir, EXPERIMENTAL_ROOT):
|
|
raise ValueError(f"dataset and output must remain below {EXPERIMENTAL_ROOT}")
|
|
marker_path = dataset_dir / "EXPERIMENTAL_ONLY.json"
|
|
yaml_path = dataset_dir / "dataset.yaml"
|
|
if not marker_path.is_file() or not yaml_path.is_file():
|
|
raise ValueError("experimental marker or dataset.yaml is missing")
|
|
marker = json.loads(marker_path.read_text(encoding="utf-8"))
|
|
if marker.get("promotion_allowed") is not False or marker.get("release_claim_allowed") is not False:
|
|
raise ValueError("experimental marker does not prohibit promotion and release claims")
|
|
if marker.get("dataset_yaml_sha256") != sha256(yaml_path):
|
|
raise ValueError("experimental dataset.yaml checksum mismatch")
|
|
return marker
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--dataset-dir", type=Path, required=True)
|
|
parser.add_argument("--base-model", type=Path, required=True)
|
|
parser.add_argument("--output-dir", type=Path, required=True)
|
|
parser.add_argument("--run-name", default="v73-flanders-remediation")
|
|
parser.add_argument("--epochs", type=int, default=20)
|
|
parser.add_argument("--imgsz", type=int, default=640)
|
|
parser.add_argument("--batch", type=int, default=8)
|
|
parser.add_argument("--learning-rate", type=float, default=0.0001)
|
|
parser.add_argument("--freeze", type=int, default=10)
|
|
parser.add_argument("--workers", type=int, default=0)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
validate_paths(args.dataset_dir, args.output_dir)
|
|
if not args.base_model.is_file():
|
|
raise SystemExit(f"base model missing: {args.base_model}")
|
|
import torch
|
|
from ultralytics import YOLO
|
|
|
|
if not torch.cuda.is_available():
|
|
raise SystemExit("CUDA is required for GeoIntel experimental training")
|
|
model = YOLO(str(args.base_model))
|
|
result = model.train(
|
|
data=str(args.dataset_dir / "dataset.yaml"),
|
|
epochs=args.epochs,
|
|
imgsz=args.imgsz,
|
|
batch=args.batch,
|
|
workers=args.workers,
|
|
device=0,
|
|
project=str(args.output_dir),
|
|
name=args.run_name,
|
|
exist_ok=False,
|
|
pretrained=True,
|
|
deterministic=True,
|
|
seed=73,
|
|
amp=False,
|
|
val=False,
|
|
lr0=args.learning_rate,
|
|
lrf=0.1,
|
|
freeze=args.freeze,
|
|
mosaic=0.0,
|
|
translate=0.05,
|
|
scale=0.1,
|
|
plots=False,
|
|
verbose=True,
|
|
)
|
|
run_dir = Path(result.save_dir)
|
|
best = run_dir / "weights" / "best.pt"
|
|
if not best.is_file():
|
|
best = run_dir / "weights" / "last.pt"
|
|
candidate = run_dir / "candidate.experimental.pt"
|
|
shutil.copy2(best, candidate)
|
|
summary = {
|
|
"schema_version": 1,
|
|
"status": "trained_experimental_only",
|
|
"promotion_allowed": False,
|
|
"release_claim_allowed": False,
|
|
"human_review_pending": True,
|
|
"dataset_yaml_sha256": sha256(args.dataset_dir / "dataset.yaml"),
|
|
"base_model": str(args.base_model),
|
|
"base_model_sha256": sha256(args.base_model),
|
|
"candidate_model": str(candidate),
|
|
"candidate_model_sha256": sha256(candidate),
|
|
"epochs": args.epochs,
|
|
"imgsz": args.imgsz,
|
|
"batch": args.batch,
|
|
"learning_rate": args.learning_rate,
|
|
"freeze": args.freeze,
|
|
"workers": args.workers,
|
|
"device": torch.cuda.get_device_name(0),
|
|
"required_next_gate": "independent frozen V72 calibration evaluation",
|
|
}
|
|
(run_dir / "experimental_training_summary.json").write_text(
|
|
json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
)
|
|
print(json.dumps(summary, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|