Brainrot_Muxa/tools/evaluate.py

281 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Оценка обобщаемости: leave-one-bag-out.
Память тоннеля обучается на всех данных, **кроме** проверяемого бэга, и только
после этого конвейер прогоняется по нему. Иначе цифры лгут: подавлять
конструкции, которые сам же и запомнил, умеет кто угодно, а на приватном тесте
будет новый участок тоннеля.
Отчёт: ложные тревоги на километр пути и доля кадров с тревогой для пустых
бэгов; для `doubleT_obstacle` — ещё и доля кадров, в которых найден настоящий
объект на ~55 м.
python tools/evaluate.py --device cuda --out artifacts/generalisation.json
"""
from __future__ import annotations
import argparse
import json
import tempfile
import numpy as np
import _bootstrap as B # noqa: F401
import _parallel as P
from flyguard.bag import Bag, find_bags
from flyguard.mushroom_body import MushroomBody, MushroomBodyConfig
from flyguard.pipeline import FlyGuard, Params
OBSTACLE_BAG = "doubleT_obstacle"
TRUE_D = (50.0, 62.0)
def _work(task):
"""Одна задача — один бэг. Память и считывание грузятся по пути уже здесь."""
path, params, limit, mem_path, rd_path = task
memory = MushroomBody.load(mem_path) if mem_path else None
readout = None
if rd_path:
from flyguard.mbon_readout import MbonReadout
readout = MbonReadout.load(rd_path)
return run_bag(path, memory, limit, params, readout=readout)
def train_excluding(per_bag: dict[str, np.ndarray], extra: np.ndarray | None,
exclude: str, target: float, device: str) -> MushroomBody:
parts = [v for k, v in per_bag.items() if k not in (exclude, OBSTACLE_BAG)]
if extra is not None:
parts.append(extra)
X = np.concatenate(parts).astype(np.float32)
mb = MushroomBody(MushroomBodyConfig())
mb.fit_normalizer(X)
mb.learn(X, rate=mb.auto_rate(X.shape[0], target), device=device)
return mb
def run_bag(bag_path, memory, limit: int, params: Params | None = None,
readout=None) -> dict:
fg = FlyGuard(params or Params(), memory=memory, readout=readout)
bag = Bag(bag_path)
n = alarms = obj_hits = fp_objects = 0
path_m = 0.0
fp_dists, times = [], []
# один и тот же лоток, попавший в треки, виден сотню кадров подряд; для
# эксплуатации важно не это, а сколько РАЗНЫХ ложных объектов возникло —
# именно столько раз поезд затормозил бы напрасно
fp_tracks: set[int] = set()
for _, pc in bag.frames(stop=limit):
res = fg.process(pc)
if res is None:
continue
n += 1
path_m += res.ego.ds if res.ego else 0.0
times.append(res.total_ms)
d = res.decision
is_obstacle_bag = bag.path.name == OBSTACLE_BAG
mine = [o for o in d.objects if TRUE_D[0] < o.distance < TRUE_D[1]] \
if is_obstacle_bag else []
others = [o for o in d.objects if o not in mine]
if mine:
obj_hits += 1
if others:
alarms += 1
fp_objects += len(others)
fp_tracks.update(o.track_id for o in others)
fp_dists.extend(o.distance for o in others)
km = max(path_m / 1000.0, 1e-6)
return dict(bag=bag.path.name, frames=n, path_m=path_m,
alarm_frames=alarms, alarm_rate=alarms / max(n, 1),
fp_objects=fp_objects, fp_tracks=len(fp_tracks),
fp_per_km=(len(fp_tracks) / km) if path_m > 5 else float("nan"),
fp_median_d=float(np.median(fp_dists)) if fp_dists else float("nan"),
obj_rate=obj_hits / max(n, 1) if bag.path.name == OBSTACLE_BAG else None,
ms_p50=float(np.median(times)) if times else 0.0,
ms_p95=float(np.percentile(times, 95)) if times else 0.0)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
ap.add_argument("--cache", default=str(B.CACHE / "tune_candidates.npz"))
ap.add_argument("--extra-cache", default=str(B.CACHE / "new_data_candidates.npz"))
ap.add_argument("--limit", type=int, default=250)
ap.add_argument("--target", type=float, default=0.4)
ap.add_argument("--device", default="cpu")
ap.add_argument("--split-adv", type=float, default=None,
help="порог разделения фигуры и фона; 0 — выключить")
ap.add_argument("--split-gap", type=float, default=None,
help="порог разреза по контрасту ламины, м; 0 — выключить")
ap.add_argument("--split-near", type=float, default=None,
help="ближе этой дальности не резать, м")
ap.add_argument("--split-top", type=int, default=None,
help="сколько фигур выносить из одной компоненты; 0 — все")
ap.add_argument("--no-acc", action="store_true", help="выключить накопитель")
ap.add_argument("--no-hab", action="store_true", help="выключить привыкание")
ap.add_argument("--mbon", default="", help="путь к обученному считыванию MBON")
ap.add_argument("--mbon-dir", default="",
help="каталог с моделями по складкам (mbon_<бэг>.npz): "
"для каждого бэга берётся модель, его не видевшая")
ap.add_argument("--mbon-blend", type=float, default=None,
help="1 — только модель, 0 — только ручная формула")
ap.add_argument("--mbon-power", type=float, default=None,
help="резкость вероятности модели")
ap.add_argument("--warn", type=float, default=None,
help="порог улики для тревоги (по умолчанию 0.5)")
ap.add_argument("--clear", type=float, default=None,
help="нижний порог гистерезиса (по умолчанию 0.3)")
ap.add_argument("--min-hits", type=int, default=None,
help="наблюдений, без которых трек не считается")
ap.add_argument("--warn-far", type=float, default=None,
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
ap.add_argument("--warn-far-from", type=float, default=None,
help="с какой дальности порог начинает падать, м")
ap.add_argument("--novelty-floor", type=float, default=None,
help="ниже этой новизны трек не считается")
ap.add_argument("--near-long", type=float, default=None,
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
ap.add_argument("--min-rays", type=int, default=None,
help="сколько лучей минимум образуют кандидата")
ap.add_argument("--min-rays-far", type=int, default=None,
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
ap.add_argument("--min-rays-far-from", type=float, default=None)
ap.add_argument("--leak-far", type=float, default=None,
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
ap.add_argument("--leak-far-from", type=float, default=None,
help="с какой дальности утечка начинает падать, м")
ap.add_argument("--nov-fade-from", type=float, default=None,
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
ap.add_argument("--nov-fade-to", type=float, default=None,
help="к какой дальности вклад знакомости обнуляется")
ap.add_argument("--mbon-prior-from", type=float, default=None,
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
ap.add_argument("--no-memory", action="store_true",
help="совсем без памяти тоннеля — так выглядит первый проезд по новой линии")
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
P.add_argument(ap)
args = ap.parse_args()
d = np.load(args.cache, allow_pickle=True)
per_bag = {str(k): d[f"X_{k}"].astype(np.float32) for k in d["names"]}
extra = None
from pathlib import Path
if Path(args.extra_cache).exists():
extra = np.load(args.extra_cache)["X"].astype(np.float32)
print(f"дополнительно в обучение: {extra.shape[0]} кандидатов из new_data")
over = {}
if args.split_adv is not None:
over["split_adv"] = args.split_adv
if args.split_gap is not None:
over["split_gap"] = args.split_gap
if args.split_near is not None:
over["split_near"] = args.split_near
if args.split_top is not None:
over["split_top"] = args.split_top
if args.no_acc:
over["enable_accumulator"] = False
if args.no_hab:
over["enable_habituation"] = False
if args.mbon_blend is not None:
over["mbon_blend"] = args.mbon_blend
if args.mbon_power is not None:
over["mbon_power"] = args.mbon_power
if args.warn is not None:
over["warn_evidence"] = args.warn
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
elif args.clear is not None:
over["clear_evidence"] = args.clear
if args.min_hits is not None:
over["min_hits"] = args.min_hits
if args.warn_far is not None:
over["warn_far"] = args.warn_far
if args.warn_far_from is not None:
over["warn_far_from"] = args.warn_far_from
if args.novelty_floor is not None:
over["novelty_floor"] = args.novelty_floor
if args.near_long is not None:
over["near_long"] = args.near_long
if args.min_rays is not None:
over["min_rays"] = args.min_rays
if args.min_rays_far is not None:
over["min_rays_far"] = args.min_rays_far
if args.min_rays_far_from is not None:
over["min_rays_far_from"] = args.min_rays_far_from
if args.leak_far is not None:
over["leak_far"] = args.leak_far
if args.leak_far_from is not None:
over["leak_far_from"] = args.leak_far_from
if args.nov_fade_from is not None:
over["nov_fade_from"] = args.nov_fade_from
if args.nov_fade_to is not None:
over["nov_fade_to"] = args.nov_fade_to
if args.mbon_prior_from is not None:
over["mbon_prior_from"] = args.mbon_prior_from
params = Params(**over)
fold_paths: dict[str, str] = {}
if args.mbon_dir:
from pathlib import Path as _P
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
fold_paths[f.stem[len("mbon_"):]] = str(f)
print(f"считывание MBON по складкам: {args.mbon_dir} "
f"({len(fold_paths)} моделей)")
elif args.mbon:
print(f"считывание MBON: {args.mbon}")
# Память обучается ЗДЕСЬ, а не в воркере: обучение идёт на видеокарте, и
# делить одну карту на пять процессов незачем. Воркеру достаётся готовый
# файл — долгая часть это проход по записи, а не обучение.
tmp = None if args.no_memory else tempfile.TemporaryDirectory(prefix="fg_mem_")
tasks, sizes = [], []
for p in find_bags(args.root):
mem_path, n_seen = "", 0
if tmp is not None:
mem = train_excluding(per_bag, extra, p.name, args.target, args.device)
mem_path = str(Path(tmp.name) / f"mem_{p.name}.npz")
mem.save(mem_path)
n_seen = int(mem.n_seen)
del mem
rd = fold_paths.get(p.name, args.mbon) if fold_paths else args.mbon
if fold_paths and p.name not in fold_paths and p.name != OBSTACLE_BAG:
print(f" внимание: для {p.name} нет своей складки")
tasks.append((p, params, args.limit, mem_path, rd))
sizes.append(n_seen)
# Задержка кадра под пятью процессами вырастает с 32 до 56 мс — это
# свойство замера, а не конвейера, и такое число нельзя показывать как
# запас по бюджету. Поэтому в параллельном режиме оно не печатается
# вовсе: перепутать nan с честным замером невозможно, а предупреждение
# в шапке пролистывается.
par = P.resolve(args.jobs, len(tasks)) > 1
if par:
print("параллельно: задержка кадра не измеряется, для неё нужен --jobs 1")
slots: list = [None] * len(tasks)
for i, task, r, secs in P.run(_work, tasks, args.jobs):
r["train_size"] = sizes[i]
if par:
r["ms_p50"] = r["ms_p95"] = float("nan")
slots[i] = r
obj = f"объект {r['obj_rate']:6.1%} | " if r["obj_rate"] is not None else ""
print(f"{r['bag']:40s} кадров {r['frames']:4d} путь {r['path_m']:6.0f} м | "
f"{obj}тревог {r['alarm_rate']:6.1%} | ложных треков {r['fp_tracks']:3d} "
f"({r['fp_per_km']:6.1f} на км) | {r['ms_p50']:5.1f}/{r['ms_p95']:5.1f} мс | "
f"{secs:5.0f} с", flush=True)
rows = [r for r in slots if r is not None]
if tmp is not None:
tmp.cleanup()
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
with open(args.out, "w", encoding="utf-8") as f:
json.dump(rows, f, ensure_ascii=False, indent=1)
empty = [r for r in rows if r["bag"] != OBSTACLE_BAG]
print("\nсводка по пустым бэгам:")
print(f" доля кадров с ложной тревогой: {np.mean([r['alarm_rate'] for r in empty]):.2%}")
fp_km = [r["fp_per_km"] for r in empty if np.isfinite(r["fp_per_km"])]
if fp_km:
print(f" разных ложных треков на километр: {np.mean(fp_km):.1f} "
f"(медиана {np.median(fp_km):.1f})")
print(f" суммарный путь: {sum(r['path_m'] for r in empty):.0f} м")
print("сохранено:", args.out)
if __name__ == "__main__":
main()