Brainrot_Muxa/tools/make_training_set.py

177 lines
8.8 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.

"""Размеченная выборка кандидатов: предмет против тоннельной обстановки.
Разметки в датасете нет, и до сих пор это определяло архитектуру: грибовидное
тело учится **без меток**, запоминая частоту обстановки. Но у нас есть
физически обоснованный генератор предметов (`flyguard.synth`), сверенный с
единственным реальным объектом: настоящий 0.67 × 1.35 м на 55 м даёт 47–69
лучей, синтетический человек 0.44 × 1.71 м на 60 м — 50. Значит, метки можно
изготовить, и изготовить достоверно.
Каждый кандидат помечается по **пересечению лучей**, а не «по дальности
примерно»: `inject` возвращает индексы лучей, в которые предмет действительно
записан, и кандидат считается предметом, если его ядро состоит из этих лучей.
Так структура тоннеля, случайно оказавшаяся на той же дальности, в
положительные не попадает.
Сценарии намеренно ставят предмет в РАЗНЫЕ точки тоннеля (`--d-starts`): замер
показал, что одна и та же дальность в разных местах перегона ведёт себя
совершенно по-разному — где-то предмет виден целиком, где-то за поворотом
(EXPERIMENTS п. 9.6). Обучаться на одной точке постановки значит выучить эту
точку.
`doubleT_obstacle` исключён целиком: там настоящий объект, и он остаётся
независимой проверкой того, что модель выучила предмет, а не «синтетику».
python tools/make_training_set.py --out data/cache/training_set.npz
"""
from __future__ import annotations
import argparse
import time
import numpy as np
import _bootstrap as B # noqa: F401
from flyguard.bag import Bag, find_bags
from flyguard.mushroom_body import FEATURES, describe
from flyguard.pipeline import FlyGuard, Params
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
def ego_track(bag: Bag, params: Params, limit: int):
"""Первый проход: пройденный путь на каждом кадре и общая решётка."""
fg = FlyGuard(params, memory=None)
s, total = [], 0.0
for _, pc in bag.frames(stop=limit):
res = fg.process(pc)
if res is None:
s.append(None)
continue
total += res.ego.ds if res.ego else 0.0
s.append(total)
return s, fg
def collect_bag(path, params: Params, limit: int, d_starts, laterals, seed: int):
bag = Bag(path)
s_track, ego_fg = ego_track(bag, params, limit)
have = [x for x in s_track if x is not None]
if len(have) < 30:
return None
s0 = have[0]
layout = ego_fg.layout_full
col0 = ego_fg.cols.start
cat = catalogue()
scen = [(n, lat, d0) for n in cat for lat in laterals for d0 in d_starts]
guide = FlyGuard(params, memory=None, layout=layout)
pipes = [FlyGuard(params, memory=None, layout=layout) for _ in scen]
rng = np.random.default_rng(seed)
X, y, dd, obj, lat_out = [], [], [], [], []
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
g = guide.process(pc)
if g is None or s_track[k] is None:
continue
s_now = s_track[k] - s0
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
for i, (name, lat, d0) in enumerate(scen):
d_true = d0 - s_now
if d_true < 6.0:
continue
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
pc2, lab = inject(pc, layout, g.plane, cat[name],
Placement(d=d_true, u=u), rng=rng, env=env)
res = pipes[i].process(pc2)
if res is None or not res.candidates:
continue
rr = lab.get("rays")
if rr is None or lab["hit_rays"] == 0:
truth = None
else:
# лучи предмета в координатах полной решётки → плоский индекс;
# отсортированный массив, а не множество: проверка идёт
# сотни тысяч раз, и `in` по множеству тут заметно дороже
truth = np.sort((rr[0].astype(np.int64) << 20)
| rr[1].astype(np.int64))
for c in res.candidates:
ii, jj = c.extra.get("rays", (None, None))
if ii is None:
continue
lbl = 0
if truth is not None and truth.size:
key = ((ii.astype(np.int64) << 20)
| (jj.astype(np.int64) + col0))
pos = np.searchsorted(truth, key)
np.clip(pos, 0, truth.size - 1, out=pos)
frac = float((truth[pos] == key).mean())
lbl = int(frac >= MIN_OVERLAP)
v = describe(c)
acc = float(c.extra.get("acc_support", 0.0)) if c.extra else 0.0
X.append(np.append(v, acc).astype(np.float32))
y.append(lbl)
dd.append(c.d)
obj.append(name if lbl else "")
lat_out.append(lat)
return (np.asarray(X, np.float32), np.asarray(y, np.int8),
np.asarray(dd, np.float32), np.asarray(obj),
np.asarray(lat_out, np.float32))
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
ap.add_argument("--out", default=str(B.CACHE / "training_set.npz"))
ap.add_argument("--limit", type=int, default=250)
ap.add_argument("--d-starts", default="80,140,200")
# Поперечные положения намеренно кроют ВЕСЬ габарит, а не только ось.
# Иначе выборка вырождается: предметы у оси, обстановка у стен, и модель
# выучивает «всё, что у оси — предмет» вместо признаков предмета. Проверено
# на пробном прогоне с одной постановкой: AUC 1.000 по всем бэгам — цифра
# красивая и бессмысленная, а в тоннеле у оси полно штатных конструкций.
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
ap.add_argument("--seed", type=int, default=20260921)
args = ap.parse_args()
d_starts = tuple(float(x) for x in args.d_starts.split(","))
laterals = tuple(float(x) for x in args.laterals.split(","))
params = Params()
B.CACHE.mkdir(parents=True, exist_ok=True)
parts = {}
for p in find_bags(args.root):
if p.name == HOLDOUT:
continue
t0 = time.time()
got = collect_bag(p, params, args.limit, d_starts, laterals, args.seed)
if got is None:
print(f" {p.name:42s} пропущен")
continue
parts[p.name] = got
Xb, yb = got[0], got[1]
print(f" {p.name:42s} {Xb.shape[0]:7d} кандидатов, "
f"предметов {int(yb.sum()):6d} ({yb.mean():5.1%}), "
f"{time.time() - t0:6.0f} с", flush=True)
if not parts:
raise SystemExit("ничего не собрано")
out = {}
for name, (Xb, yb, db, ob, lb) in parts.items():
out[f"X_{name}"] = Xb
out[f"y_{name}"] = yb
out[f"d_{name}"] = db
out[f"obj_{name}"] = ob
out[f"lat_{name}"] = lb
np.savez_compressed(args.out, names=np.array(list(parts)),
features=np.array(FEATURES + ("acc_support",)), **out)
tot = sum(v[0].shape[0] for v in parts.values())
pos = sum(int(v[1].sum()) for v in parts.values())
print(f"\nвсего {tot} кандидатов, предметов {pos} ({pos / tot:.1%})")
print("сохранено:", args.out)
if __name__ == "__main__":
main()