121 lines
6.8 KiB
Python
121 lines
6.8 KiB
Python
"""Считывание по ТРЕКУ — тот же контур MBON, но на входе накопленная история.
|
||
|
||
Считывание в `mbon_readout.py` смотрит на один кадр: контур кандидата, его
|
||
тень, яркость, опору накопителя. Этого хватает вблизи и не хватает вдали —
|
||
за 120 м предмет даёт пять-шесть лучей, и по одному кадру он неотличим от
|
||
куска стены, попавшего в габарит.
|
||
|
||
Но у трека есть то, чего у кандидата нет в принципе: **история**. Поезд
|
||
проехал двадцать метров, а предмет, если он настоящий, остался на том же
|
||
месте в тоннеле. Воронка потерь (EXPERIMENTS п. 12) показывает, что именно
|
||
здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 %
|
||
наблюдений, а до решения доходит 38 % — улика есть, порога не набирает.
|
||
|
||
Поэтому вход здесь другой:
|
||
|
||
* `s_std` — разброс места в тоннеле, которое подразумевают наблюдения.
|
||
У неподвижного предмета оно постоянно по построению, у ложного трека,
|
||
собранного из разных кусков стены, — гуляет. В улику этот признак
|
||
сейчас не входит вообще, ни одним множителем;
|
||
* `rays_slope` — растёт ли число лучей так, как обязано расти при
|
||
сближении. У предмета оно идёт по закону решётки, у случайного
|
||
совпадения — как придётся;
|
||
* сводки веса наблюдения и покадрового отсчёта MBON: среднее и максимум
|
||
за всю жизнь трека, а не последнее значение.
|
||
|
||
Схема при этом не меняется: те же клетки Кеньона по шесть когтей, то же
|
||
торможение APL, тот же один выход. Это ровно та же операция, что MBON
|
||
проделывает над кандидатом, только этажом выше — над тем, что центральный
|
||
комплекс успел накопить. У мухи так же: нисходящие нейроны получают сходящийся
|
||
вход и от зрительных путей, и от центрального комплекса, а не только от сетчатки.
|
||
|
||
Стоимость — одно умножение матрицы на трек, и только для тех треков, что уже
|
||
прошли дешёвые проверки в `descending.py`.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
|
||
from .central_complex import Track, expected_rays
|
||
from .mbon_readout import MbonConfig, MbonReadout
|
||
|
||
TRACK_FEATURES = (
|
||
"log_d", # дальность сейчас
|
||
"evidence", # накопленная улика — то, чем решают сегодня
|
||
"hits", # подтверждений
|
||
"age", # возраст, кадров
|
||
"hit_rate", # подтверждений на кадр жизни
|
||
"misses", # промахов подряд
|
||
"closed", # log(первая дальность / нынешняя): насколько подъехали
|
||
"novelty", # сглаженная новизна из памяти тоннеля
|
||
"width", "height", "h",
|
||
"speed_lat", # модуль поперечной скорости
|
||
"rays_now", # лучей к ожидаемым для этой дальности
|
||
"rays_mean", # то же, в среднем за жизнь
|
||
"rays_slope", # и наклон по времени
|
||
"w_mean", "w_max", # вес наблюдения
|
||
"p_mean", "p_max", # покадровый отсчёт считывания
|
||
"s_std", # разброс места в тоннеле, м
|
||
"u_std", # разброс поперечного положения, м
|
||
)
|
||
|
||
|
||
def _std(n: int, s: float, sq: float) -> float:
|
||
"""Разброс по накопленным суммам. Отрицательную дисперсию даёт
|
||
округление, а не данные, поэтому она просто отсекается."""
|
||
if n < 2:
|
||
return 0.0
|
||
var = sq / n - (s / n) ** 2
|
||
return float(np.sqrt(var)) if var > 0.0 else 0.0
|
||
|
||
|
||
def describe_track(t: Track, s_now: float) -> np.ndarray:
|
||
"""Вектор трека для считывания. Порядок — как в `TRACK_FEATURES`."""
|
||
d = max(t.distance(s_now), 1.0)
|
||
n = max(t.n_obs, 1)
|
||
den = t.n_obs * t.i_sq - t.i_sum * t.i_sum
|
||
slope = ((t.n_obs * t.ri_sum - t.i_sum * t.r_sum) / den
|
||
if t.n_obs > 1 and abs(den) > 1e-9 else 0.0)
|
||
return np.array((
|
||
np.log(d),
|
||
t.evidence,
|
||
float(t.hits),
|
||
float(t.age),
|
||
t.hits / max(t.age, 1),
|
||
float(t.misses),
|
||
np.log(max(t.first_d, 1.0) / d),
|
||
t.novelty,
|
||
t.width, t.height, t.h,
|
||
abs(t.speed_lat),
|
||
t.last_n_rays / expected_rays(d),
|
||
t.r_sum / n,
|
||
slope,
|
||
t.w_sum / n, t.w_max,
|
||
t.p_sum / n, t.p_max,
|
||
_std(t.n_obs, t.s_sum, t.s_sq),
|
||
_std(t.n_obs, t.u_sum, t.u_sq),
|
||
), np.float32)
|
||
|
||
|
||
class TrackReadout(MbonReadout):
|
||
"""Тот же разрежённый код, другой вход и другой учитель.
|
||
|
||
Ёмкость меньше, чем у покадрового считывания: примеров тут на порядок
|
||
меньше (треков, а не кандидатов), и восемь тысяч клеток на такой выборке
|
||
заучивают её наизусть. Значение выбирается развёрткой в
|
||
`tools/train_track.py`, а не на глаз.
|
||
"""
|
||
|
||
def __init__(self, cfg: MbonConfig | None = None,
|
||
n_pn: int = len(TRACK_FEATURES)):
|
||
super().__init__(cfg or MbonConfig(n_kc=2_000, seed=20260922),
|
||
n_pn=n_pn)
|
||
|
||
def annotate_tracks(self, tracks: list[Track], s_now: float) -> None:
|
||
"""Проставить каждому треку отсчёт в `extra`-поле `p_track`."""
|
||
if not tracks:
|
||
return
|
||
X = np.stack([describe_track(t, s_now) for t in tracks])
|
||
p = 1.0 / (1.0 + np.exp(-self.logit(X)))
|
||
for t, pp in zip(tracks, p):
|
||
t.p_track = float(pp)
|