Lidar_Muxa/tools/train_track.py
Zhirik1337 7507a123e5 GPU-обвязка NVIDIA CUDA 12 и ускорение ML-конвейера
- flyguard/device.py: автоопределение NVIDIA GPU, сбор телеметрии и Graceful Fallback на CPU;
- flyguard/lamina.py: ускорение 2D DoG фильтрации на тензорах PyTorch CUDA (0.25 мс вместо 8 мс);
- flyguard/mbon_readout.py: векторизованный GPU-цикл обучения MBON без CPU-синхронизаций (поддержка 50k-100k клеток Кеньона);
- flyguard/mushroom_body.py, flyguard/track_readout.py: аппаратное ускорение и опция GPU-бустинга;
- flyguard/pipeline.py, tools/*.py: сквозная поддержка параметра device='auto' для всех инструментов;
- Dockerfile.gpu, requirements-gpu.txt, docker-compose.yml: MLOps-инфраструктура под NVIDIA RTX 4070 Ti Super 16GB;
- tests/test_pipeline.py: добавлены юнит-тесты на GPU device detection и роутинг ламины (40 тестов PASS).
2026-09-22 22:06:44 +03:00

225 lines
12 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.

"""Обучение считывания по ТРЕКАМ и честная проверка по бэгам.
Покадровое считывание (`train_mbon.py`) отвечает на вопрос «похоже ли это
пятно на предмет». Здесь вопрос другой: «стоит ли по этому треку тормозить» —
и отвечать на него по одному кадру нечем. Воронка потерь (EXPERIMENTS п. 12)
показывает, где это болит: на 120…160 м трек заводится в 62 % наблюдений, а до
решения доходит 38 %. Улика есть, порога не набирает.
Главная таблица внизу — не AUC модели сам по себе, а **сравнение с уликой**.
Улика уже сейчас стоит в пороге, и если модель её не обгоняет, весь замысел
не стоит ни строчки кода: значит, в истории трека нет ничего сверх того, что
накопитель и так посчитал.
Проверка — leave-one-bag-out, как и везде: модель обучается на всех записях,
кроме проверяемой.
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
--mbon-dir artifacts/mbon_folds --tracks-out data/cache/track_set.npz
python tools/train_track.py --device cuda
"""
from __future__ import annotations
import argparse
import time
from pathlib import Path
import numpy as np
import _bootstrap as B # noqa: F401
from _metrics import auc, fpr_at_tpr
from flyguard.mbon_readout import MbonConfig
from flyguard.track_readout import TRACK_FEATURES, TrackReadout
BANDS = ((0, 30), (30, 55), (55, 80), (80, 110), (110, 160), (160, 230))
def band_table(d, y, cols: dict[str, np.ndarray]) -> None:
"""AUC по полосам дальности для нескольких оценок разом."""
head = "".join(f"{k:>16}" for k in cols)
print(f"{'полоса':>12}{'наблюдений':>12}{'предметов':>11}{head}")
for lo, hi in BANDS:
m = (d >= lo) & (d < hi)
if m.sum() < 30 or len(set(y[m].tolist())) < 2:
continue
cells = "".join(f"{auc(v[m], y[m]):16.3f}" for v in cols.values())
print(f"{f'{lo}-{hi} м':>12}{int(m.sum()):12d}{int(y[m].sum()):11d}{cells}")
def loo(names, X, Y, cfg, args):
"""Обучение на всех записях, кроме проверяемой."""
out = []
for held in names:
tr = [n for n in names if n != held]
Xtr = np.concatenate([X[n] for n in tr])
ytr = np.concatenate([Y[n] for n in tr])
m = TrackReadout(cfg, n_pn=Xtr.shape[1])
m.fit_normalizer(Xtr)
m.learn(Xtr, ytr, epochs=args.epochs, lr=args.lr, l2=args.l2,
device=args.device)
out.append((held, m.score(X[held]), m))
return out
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--data", default=str(B.CACHE / "track_set.npz"))
ap.add_argument("--out", default=str(B.ARTIFACTS / "track_readout.npz"))
ap.add_argument("--save-folds", default=str(B.ARTIFACTS / "track_folds"))
ap.add_argument("--n-kc", type=int, default=0,
help="ёмкость итоговой модели; 0 — лучшая из развёртки")
ap.add_argument("--sweep-kc", default="500,1000,2000,4000")
ap.add_argument("--epochs", type=int, default=200)
ap.add_argument("--lr", type=float, default=2.0)
ap.add_argument("--device", default="auto",
help="устройство обучения ('auto', 'cuda', 'cpu')")
ap.add_argument("--baseline", action="store_true",
help="сравнить с GPU/CPU градиентным бустингом (LightGBM)")
ap.add_argument("--drop", default="",
help="признаки, выброшенные из обучения — это РЕЖИМ ЗАМЕРА, "
"модель при этом не сохраняется: конвейер считает "
"описание целиком и укороченную модель не примет. "
"Смысл — проверить вклад признака. Так, оба разброса "
"на полигоне выглядят "
"сильными (-s_std даёт AUC 0.869 за 160 м), а на "
"настоящем объекте ПЕРЕВЁРНУТЫ — 0.365 м у предмета "
"против 0.013 м у ложных треков. Вставленный предмет "
"стоит на дальности, посчитанной из той же оценки "
"собственного движения, что двигает мировую "
"координату, поэтому разброса у него нет по "
"построению. EXPERIMENTS п. 15.1")
ap.add_argument("--min-hits", type=int, default=2,
help="как в descending.py: трек с меньшим числом "
"подтверждений всё равно не рассматривается")
args = ap.parse_args()
d = np.load(args.data, allow_pickle=True)
names = [str(n) for n in d["names"]]
feats = [str(f) for f in d["features"]]
assert feats == list(TRACK_FEATURES), "выборка собрана другим дескриптором"
# Модель, обученная без части признаков, не подойдёт конвейеру: он
# считает описание целиком. Такая модель — инструмент замера, и в
# `descending.py` она включается только явным `--track-score model`.
i_hits = feats.index("hits")
i_ev = feats.index("evidence")
X, Y, D = {}, {}, {}
for n in names:
Xb, yb, db = d[f"X_{n}"], d[f"y_{n}"].astype(np.int8), d[f"d_{n}"]
keep = Xb[:, i_hits] >= args.min_hits
X[n], Y[n], D[n] = Xb[keep].astype(np.float32), yb[keep], db[keep]
drop = [s.strip() for s in args.drop.split(",") if s.strip()]
if drop:
bad = [s for s in drop if s not in feats]
if bad:
raise SystemExit(f"нечего выбрасывать: {bad}")
cols = [k for k, f in enumerate(feats) if f not in drop]
feats = [feats[k] for k in cols]
i_hits, i_ev = feats.index("hits"), feats.index("evidence")
for n in names:
X[n] = X[n][:, cols]
print("выброшено из обучения:", ", ".join(drop))
tot = sum(v.shape[0] for v in X.values())
pos = sum(int(v.sum()) for v in Y.values())
print(f"выборка: {tot} наблюдений трека, предметных {pos} "
f"({pos / max(tot, 1):.1%}), признаков {len(feats)}, "
f"подтверждений не меньше {args.min_hits}")
for n in names:
print(f" {n:<42}{X[n].shape[0]:7d} предметных {int(Y[n].sum()):6d} "
f"({Y[n].mean():5.1%})")
Xall = np.concatenate([X[n] for n in names])
yall = np.concatenate([Y[n] for n in names])
dall = np.concatenate([D[n] for n in names])
# ------------------------------------------- что даёт каждый признак сам
# Ловля артефактов: если наверх вылезает признак, который не может нести
# физику (возраст трека, ширина), — выборка перекошена, а не модель умна.
print("\nодин признак — какой AUC, вся выборка:")
rank = sorted(((auc(Xall[:, i], yall), f) for i, f in enumerate(feats)),
key=lambda p: -abs(p[0] - 0.5))
for a, f in rank:
print(f" {f:<12}{a:6.3f}")
# ------------------------------------------------------------- развёртка
sweep = [int(x) for x in args.sweep_kc.split(",") if x.strip()]
best, best_auc = args.n_kc or 2000, -1.0
if sweep and not args.n_kc:
print("\nразвёртка по ёмкости (leave-one-bag-out):")
for n_kc in sweep:
cfg = MbonConfig(n_kc=n_kc, sparsity=100.0 / n_kc, seed=20260922)
t0 = time.time()
res = loo(names, X, Y, cfg, args)
s = np.concatenate([r[1] for r in res])
yy = np.concatenate([Y[r[0]] for r in res])
a = auc(s, yy)
print(f" {n_kc:6d} клеток: AUC {a:.4f}, "
f"ложных при 95 % пойманных {fpr_at_tpr(s, yy):.3f}, "
f"{time.time() - t0:5.1f} с")
if a > best_auc:
best, best_auc = n_kc, a
cfg = MbonConfig(n_kc=best, sparsity=100.0 / best, seed=20260922)
print(f"\nитоговая ёмкость: {best} клеток")
res = loo(names, X, Y, cfg, args)
s_loo = np.concatenate([r[1] for r in res])
y_loo = np.concatenate([Y[r[0]] for r in res])
d_loo = np.concatenate([D[r[0]] for r in res])
ev_loo = np.concatenate([X[r[0]][:, i_ev] for r in res])
print(f"\nleave-one-bag-out: AUC {auc(s_loo, y_loo):.4f}, "
f"ложных при 95 % пойманных {fpr_at_tpr(s_loo, y_loo):.3f}")
for held, s, _ in res:
print(f" {held:<42}AUC {auc(s, Y[held]):.4f}")
table_cols = {"улика: AUC": ev_loo, "модель: AUC": s_loo}
if args.baseline:
import lightgbm as lgb
s_lgb = []
for held in names:
tr = [n for n in names if n != held]
Xtr = np.concatenate([X[n] for n in tr])
ytr = np.concatenate([Y[n] for n in tr])
dev_type = "gpu" if (args.device == "cuda" or (args.device == "auto" and B.Path("/dev/nvidia0").exists())) else "cpu"
try:
g = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, num_leaves=31,
device=dev_type, verbose=-1)
g.fit(Xtr, ytr)
except Exception:
g = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, num_leaves=31,
verbose=-1)
g.fit(Xtr, ytr)
s_lgb.append(g.predict_proba(X[held])[:, 1])
s_lgb_arr = np.concatenate(s_lgb)
print(f"\nбустинг leave-one-bag-out: AUC {auc(s_lgb_arr, y_loo):.4f}, "
f"ложных при 95 % пойманных {fpr_at_tpr(s_lgb_arr, y_loo):.3f}")
table_cols["бустинг: AUC"] = s_lgb_arr
# ------------------------------- главное: обгоняет ли модель саму улику
print("\nмодель против улики, по полосам дальности:")
band_table(d_loo, y_loo, table_cols)
# --------------------------------------------------------------- хранение
if drop:
print()
print("режим замера (--drop): модель не сохраняется")
return
folds = Path(args.save_folds)
folds.mkdir(parents=True, exist_ok=True)
for held, _, m in res:
m.save(folds / f"track_{held}.npz")
print(f"\nмодели по складкам: {folds} ({len(res)} шт.)")
final = TrackReadout(cfg, n_pn=Xall.shape[1])
final.fit_normalizer(Xall)
final.learn(Xall, yall, epochs=args.epochs, lr=args.lr, l2=args.l2,
device=args.device)
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
final.save(args.out)
print("сохранено:", args.out)
if __name__ == "__main__":
main()