forked from Dan4ick/Lidar_Muxa
слияние с origin/main: интеграция жёсткого порога по треку (track_gate) с алгоритмическими улучшениями и докером
This commit is contained in:
commit
f5cbe2af17
9 changed files with 285 additions and 35 deletions
|
|
@ -87,7 +87,7 @@ flyguard/ ядро: стадии обработки, память, сч
|
||||||
pipeline.py сборка
|
pipeline.py сборка
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор
|
tools/ обучение, оценка, разбор
|
||||||
tests/ 32 теста, запускаются без данных и без ROS
|
tests/ 34 теста, запускаются без данных и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1747,3 +1747,197 @@ P@150 0.32), а на всём, что меньше, — заметно хуже:
|
||||||
И отдельно: о ложных тревогах в той работе не сказано **ничего** — ни одного
|
И отдельно: о ложных тревогах в той работе не сказано **ничего** — ни одного
|
||||||
упоминания false positive или false alarm. У нас они измерены на двух сценах
|
упоминания false positive или false alarm. У нас они измерены на двух сценах
|
||||||
и выражены в треках на километр пути.
|
и выражены в треках на километр пути.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Решение по треку, а не по кадру
|
||||||
|
|
||||||
|
Воронка потерь (п. 12) разделила потери на две болезни, и вторая до сих пор не
|
||||||
|
лечилась: на 90…160 м трек **заводится, а решения не даёт**. В базовой настройке
|
||||||
|
это 0.20 и 0.24 от видимых наблюдений — больше, чем теряется на всех остальных
|
||||||
|
ступенях вместе.
|
||||||
|
|
||||||
|
Порогом это не чинится, и это уже измерено (п. 12.4): 0.5 → 0.3 не меняет
|
||||||
|
ничего, потому что улика у далёкого трека не «чуть ниже порога», а около нуля.
|
||||||
|
Значит, дело не в пороге, а в том, **чем** мы меряем трек.
|
||||||
|
|
||||||
|
### 15.1. Улика суммируется и упирается в потолок
|
||||||
|
|
||||||
|
Улика — это `min(1, Σ gain·w)` с утечкой за промахи. Два её свойства работают
|
||||||
|
против нас:
|
||||||
|
|
||||||
|
* она **суммируется**, то есть отвечает на вопрос «давно ли я на это смотрю», а
|
||||||
|
не «на что именно я смотрю»;
|
||||||
|
* она **обрезана единицей**, и у всего, что наблюдается дольше пары секунд, она
|
||||||
|
просто равна 1.
|
||||||
|
|
||||||
|
Второе проверяется не на синтетике, а на единственной записи с настоящим
|
||||||
|
объектом — `doubleT_obstacle`, 753 наблюдения трека, из них 188 приходится на
|
||||||
|
реальный объект 0.67 × 1.35 м на 55 м
|
||||||
|
(`tools/check_obstacle.py --tracks --memory ... --mbon artifacts/mbon_folds/mbon_roundT_doubleT.npz`):
|
||||||
|
|
||||||
|
| признак | объект | ложные треки | AUC |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **улика** | **1.000** | **1.000** | **0.624** |
|
||||||
|
| средний вес наблюдения `w_mean` | 0.998 | 0.269 | 1.000 |
|
||||||
|
| средний отсчёт считывания `p_mean` | 0.998 | 0.582 | 1.000 |
|
||||||
|
| максимум отсчёта `p_max` | 1.000 | 0.782 | 1.000 |
|
||||||
|
| лучей к ожидаемым, в среднем | 6.483 | 1.384 | 1.000 |
|
||||||
|
| возраст трека, кадров | 94.5 | 52.0 | 0.679 |
|
||||||
|
|
||||||
|
Улика различает предмет и ложные треки с AUC 0.624 — чуть лучше монетки, — и
|
||||||
|
не потому, что сигнала нет, а потому что и у того, и у другого она упёрлась в
|
||||||
|
потолок. **Тот самый вес, из которого улика складывается, различает их
|
||||||
|
идеально.** Сумма потеряла то, что было в слагаемых.
|
||||||
|
|
||||||
|
Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со
|
||||||
|
средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как
|
||||||
|
раньше, так что размен меряется, а не объявляется.
|
||||||
|
|
||||||
|
### 15.2. Выборка по трекам
|
||||||
|
|
||||||
|
Полигон научили попутно выкладывать описание каждого живого трека на каждом
|
||||||
|
кадре с меткой «это вставленный предмет» (`make_benchmark.py --tracks-out`).
|
||||||
|
Собирается это внутри полигона нарочно: та же память тоннеля, то же покадровое
|
||||||
|
считывание, те же пороги — обученное на другой обстановке считывание нечего и
|
||||||
|
мерить. Вышло 10 819 наблюдений трека, после отбора «не меньше двух
|
||||||
|
подтверждений» (столько же требует `descending.py`) — 9 299, предметных 42.1 %.
|
||||||
|
|
||||||
|
Что даёт каждый признак сам по себе, по полосам дальности:
|
||||||
|
|
||||||
|
| полоса | набл. | улика | `p_mean` | `p_max` | `w_mean` | `−s_std` |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 0–30 м | 2208 | 0.962 | 0.954 | 0.956 | 0.946 | 0.876 |
|
||||||
|
| 30–55 м | 1216 | 0.892 | **0.989** | 0.924 | 0.965 | 0.769 |
|
||||||
|
| 55–80 м | 1474 | 0.983 | 0.949 | **0.994** | 0.914 | 0.562 |
|
||||||
|
| 80–110 м | 1777 | 0.688 | 0.801 | **0.845** | 0.832 | 0.459 |
|
||||||
|
| 110–160 м | 1977 | 0.724 | 0.849 | 0.815 | **0.900** | 0.694 |
|
||||||
|
| 160–230 м | 647 | 0.306 | 0.218 | 0.218 | 0.218 | **0.869** |
|
||||||
|
| всё | 9299 | 0.865 | 0.895 | 0.890 | 0.880 | 0.670 |
|
||||||
|
|
||||||
|
Главное здесь — полосы 80…160 м, ровно те, где воронка и теряет: улика 0.688 и
|
||||||
|
0.724, а история наблюдений 0.832 и 0.900.
|
||||||
|
|
||||||
|
### 15.3. Почему за 160 м всё переворачивается
|
||||||
|
|
||||||
|
В последней полосе AUC 0.218…0.306 — то есть признаки указывают **в обратную
|
||||||
|
сторону**. Разбор показывает, что дело не в признаках:
|
||||||
|
|
||||||
|
| | наблюдений | возраст | подтверждений | улика | `p_mean` |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| настоящий предмет | 114 | 2.0 | 2.0 | 0.094 | 0.104 |
|
||||||
|
| ложные треки | 533 | 5.0 | 4.0 | 0.234 | 0.351 |
|
||||||
|
|
||||||
|
На самом краю дальности предмет — **всегда самое молодое, что есть в сцене**:
|
||||||
|
он только что вошёл в зону видимости, а всё, что уже ведётся, ведётся дольше.
|
||||||
|
Любая величина, растущая со временем наблюдения, там обязана ранжировать
|
||||||
|
наоборот. Это не лечится ни моделью, ни порогом; это граница метода, и её надо
|
||||||
|
называть, а не прятать.
|
||||||
|
|
||||||
|
### 15.4. Привязка к месту: признак, который врёт на синтетике
|
||||||
|
|
||||||
|
Самым заманчивым признаком казался `s_std` — разброс места в тоннеле, которое
|
||||||
|
подразумевает наблюдение (пройденный путь плюс измеренная дальность). У
|
||||||
|
неподвижного предмета оно обязано быть постоянным. На полигоне признак и
|
||||||
|
выглядел блестяще: за 160 м AUC 0.869 при том, что все остальные там
|
||||||
|
перевёрнуты, разброс 0.01 м у предмета против 1.08 м у ложных треков.
|
||||||
|
|
||||||
|
На настоящем объекте он **перевёрнут**: 0.365 м у предмета против 0.013 м у
|
||||||
|
ложных треков, AUC 0.223.
|
||||||
|
|
||||||
|
Причина в устройстве вставки. Синтетический предмет ставится на дальность
|
||||||
|
`d_start − s(t)`, где `s(t)` — тот же самый проинтегрированный путь, который
|
||||||
|
двигает мировую координату треков. Ошибка оценки собственного движения входит в
|
||||||
|
обе величины и сокращается: у вставленного предмета привязка не дрожит **по
|
||||||
|
построению**. У настоящего объекта она дрожит на реальную ошибку одометрии, а
|
||||||
|
у ложных треков, прилипших к геометрии тоннеля, — не дрожит вовсе.
|
||||||
|
|
||||||
|
Это тот же класс ошибки, что артефакт яркости (п. 11.1), и найден он тем же
|
||||||
|
способом: цифру с полигона проверили на единственной реальной метке, какая
|
||||||
|
есть. Оба разброса (`s_std`, `u_std`) выброшены из обучения; в дескрипторе они
|
||||||
|
оставлены нарочно, чтобы проверку можно было повторить.
|
||||||
|
|
||||||
|
### 15.5. Обученное считывание по треку — отрицательный результат
|
||||||
|
|
||||||
|
Сделали то же, что с покадровым считыванием: те же клетки Кеньона, то же
|
||||||
|
торможение APL, один выход, вход — 21 признак трека
|
||||||
|
(`flyguard/track_readout.py`, `tools/train_track.py`). Проверка —
|
||||||
|
leave-one-bag-out.
|
||||||
|
|
||||||
|
| ёмкость | AUC |
|
||||||
|
|---|---|
|
||||||
|
| 500 клеток | 0.8713 |
|
||||||
|
| 1000 | 0.8641 |
|
||||||
|
| 2000 | 0.8651 |
|
||||||
|
| 4000 | 0.8730 |
|
||||||
|
| 4000, без обоих разбросов | 0.8801 |
|
||||||
|
|
||||||
|
Для сравнения: улика сама по себе даёт 0.865, `w_mean` — 0.880, `p_mean` —
|
||||||
|
0.895, произведение `p_mean · улика` — 0.904.
|
||||||
|
|
||||||
|
**Модель не обгоняет ни один из признаков, которые ей же и скормили.** По
|
||||||
|
полосам она выигрывает у улики на 30…55 и 80…110 м и проигрывает на 55…80 и
|
||||||
|
за 160 м. Причина понятна и её стоит назвать: наблюдений 9 299, но они не
|
||||||
|
независимы — это сотни кадров подряд по одному и тому же треку. Независимых
|
||||||
|
треков тут сотни, а не тысячи, и разрежённому коду на такой выборке учиться
|
||||||
|
нечему.
|
||||||
|
|
||||||
|
Вывод: считывание по трекам остаётся в репозитории как инструмент замера, в
|
||||||
|
решении **не участвует** (`track_score` по умолчанию `w_mean`, модель
|
||||||
|
включается только явным `--track-score model`). Цена вопроса — один вечер и
|
||||||
|
28 секунд обучения; сэкономлено то, что модель не поехала в продукт.
|
||||||
|
|
||||||
|
### 15.6. Размен: что стоит смешивание
|
||||||
|
|
||||||
|
Полигон, `человек_стоя`, память тоннеля на месте, считывание по складкам.
|
||||||
|
«Посторонних» — тревоги, не совпавшие с вставленным предметом, на 14 004
|
||||||
|
наблюдения.
|
||||||
|
|
||||||
|
| настройка | P@50 | P@100 | P@150 | посторонних |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| улика одна (`track_blend = 0`) | 0.70 | 0.53 | 0.33 | 999 |
|
||||||
|
| смешивание 0.3 | 0.71 | 0.43 | 0.26 | **354** |
|
||||||
|
| смешивание 0.5 | 0.63 | 0.17 | 0.10 | 305 |
|
||||||
|
| смешивание 0.7 | 0.63 | 0.17 | 0.10 | 296 |
|
||||||
|
|
||||||
|
Колено видно сразу: за 0.3 ложные почти не падают (354 → 305 → 296), а
|
||||||
|
обнаружение рушится. Причина арифметическая, и её стоит назвать: обе величины
|
||||||
|
меньше единицы, поэтому их произведение **всегда ниже улики**, и при
|
||||||
|
неподвижном пороге смешивание работает как его повышение. Сравнивать настройки
|
||||||
|
имеет смысл только на выровненной рабочей точке — то есть опустив порог на
|
||||||
|
столько, на сколько смешивание опустило отсчёт.
|
||||||
|
|
||||||
|
Полезная доля смешивания, таким образом, мала, и весь вопрос в том, на что
|
||||||
|
потратить освободившиеся три четверти ложных тревог.
|
||||||
|
|
||||||
|
### 15.7. Жёсткий порог вместо смешивания — отрицательный результат
|
||||||
|
|
||||||
|
Смешивание `w_mean^b · улика^(1−b)` опускает отсчёт **и предмету тоже**, потому
|
||||||
|
что обе величины меньше единицы. Напрашивается вместо него порог: трек с
|
||||||
|
низким средним весом просто не рассматривать, а прошедшему не отнимать ничего.
|
||||||
|
Тем более что на реальном объекте разделение выглядит идеальным — 0.998 против
|
||||||
|
0.269.
|
||||||
|
|
||||||
|
Замер (полигон, `человек_стоя`):
|
||||||
|
|
||||||
|
| настройка | P@50 | P@100 | P@150 | посторонних |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| база | 0.70 | 0.53 | 0.33 | 999 |
|
||||||
|
| порог 0.40 | 0.60 | **0.03** | 0.10 | 158 |
|
||||||
|
| порог 0.55 | 0.37 | **0.00** | 0.01 | 85 |
|
||||||
|
|
||||||
|
Ложные тревоги падают в шесть и в двенадцать раз, а обнаружение исчезает.
|
||||||
|
Воронка показывает где: в полосе 90…120 м до решения доходит **0.02** от
|
||||||
|
видимых наблюдений против 0.49 в базе.
|
||||||
|
|
||||||
|
Причина — та же, что у всех остальных порогов в этой работе: **неподвижный
|
||||||
|
порог слеп к дальности**. Средний вес наблюдения сам падает с расстоянием, и
|
||||||
|
сильно: в него входит контраст к фону, который за 140 м структурно равен нулю
|
||||||
|
(п. 7.3), и вероятность считывания, которая падает вместе с числом лучей.
|
||||||
|
Порог 0.55 отсекает не «плохие треки», а «всё, что дальше семидесяти метров».
|
||||||
|
|
||||||
|
Чтобы порог тут работал, его пришлось бы нормировать на ожидаемый для этой
|
||||||
|
дальности вес — то есть сделать ровно то, что уже сделано для числа лучей
|
||||||
|
(п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но
|
||||||
|
смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой,
|
||||||
|
поэтому порог оставлен выключенным (`track_gate = 0`).
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class Track:
|
||||||
w_max: float = 0.0
|
w_max: float = 0.0
|
||||||
p_sum: float = 0.0 # отсчёт считывания по кандидату, если оно есть
|
p_sum: float = 0.0 # отсчёт считывания по кандидату, если оно есть
|
||||||
p_max: float = 0.0
|
p_max: float = 0.0
|
||||||
|
p_n: int = 0 # сколько раз он вообще был
|
||||||
w_ema: float = 0.0 # он же, но со скользящим забыванием
|
w_ema: float = 0.0 # он же, но со скользящим забыванием
|
||||||
r_sum: float = 0.0 # лучи, нормированные на ожидаемые для дальности
|
r_sum: float = 0.0 # лучи, нормированные на ожидаемые для дальности
|
||||||
r_last: float = 0.0
|
r_last: float = 0.0
|
||||||
|
|
@ -86,8 +87,13 @@ class Track:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def p_mean(self) -> float:
|
def p_mean(self) -> float:
|
||||||
"""Средний покадровый отсчёт считывания MBON за жизнь трека."""
|
"""Средний покадровый отсчёт считывания MBON за жизнь трека.
|
||||||
return self.p_sum / max(self.n_obs, 1)
|
|
||||||
|
Без переданной конвейеру модели отсчёта нет вовсе, и возвращать тут
|
||||||
|
ноль значило бы тихо погасить все треки разом. Поэтому в таком случае
|
||||||
|
отдаётся средний вес наблюдения — он считается всегда.
|
||||||
|
"""
|
||||||
|
return self.p_sum / self.p_n if self.p_n else self.w_mean
|
||||||
|
|
||||||
def distance(self, s_now: float) -> float:
|
def distance(self, s_now: float) -> float:
|
||||||
return self.s_world - s_now
|
return self.s_world - s_now
|
||||||
|
|
@ -95,11 +101,15 @@ class Track:
|
||||||
def observe(self, c: Candidate, s_obs: float, w: float) -> None:
|
def observe(self, c: Candidate, s_obs: float, w: float) -> None:
|
||||||
"""Записать наблюдение в сводку.
|
"""Записать наблюдение в сводку.
|
||||||
|
|
||||||
`s_obs` — место в тоннеле, которое подразумевает это наблюдение:
|
Полезное здесь — `w_sum` и `p_sum`: улика суммируется и упирается в
|
||||||
пройденный путь плюс измеренная дальность. У НАСТОЯЩЕГО неподвижного
|
потолок, а среднее того же веса различает предмет и обстановку и после
|
||||||
предмета оно не должно меняться от кадра к кадру, как бы поезд ни
|
насыщения (EXPERIMENTS п. 15.1).
|
||||||
ехал, и разброс этой величины — самая прямая проверка гипотезы,
|
|
||||||
какая у трека вообще есть. В улику она сейчас не входит никак.
|
`s_obs` — место в тоннеле, которое подразумевает наблюдение: путь
|
||||||
|
плюс дальность. Копится ТОЛЬКО ради замеров: на полигоне его разброс
|
||||||
|
выглядит сильным признаком, а на настоящем объекте он перевёрнут,
|
||||||
|
потому что вставка стоит на дальности, посчитанной из той же оценки
|
||||||
|
движения (п. 15.4). В решении не участвует и участвовать не должен.
|
||||||
"""
|
"""
|
||||||
i = float(self.n_obs)
|
i = float(self.n_obs)
|
||||||
self.n_obs += 1
|
self.n_obs += 1
|
||||||
|
|
@ -115,9 +125,11 @@ class Track:
|
||||||
# Скользящее забывание (полупериод около трёх наблюдений) отвечает
|
# Скользящее забывание (полупериод около трёх наблюдений) отвечает
|
||||||
# на вопрос «каков он сейчас», а не «каким был с самого начала».
|
# на вопрос «каков он сейчас», а не «каким был с самого начала».
|
||||||
self.w_ema = w if self.n_obs == 1 else 0.8 * self.w_ema + 0.2 * w
|
self.w_ema = w if self.n_obs == 1 else 0.8 * self.w_ema + 0.2 * w
|
||||||
p = float(c.extra.get("mbon", 0.0)) if c.extra else 0.0
|
p = c.extra.get("mbon") if c.extra else None
|
||||||
self.p_sum += p
|
if p is not None:
|
||||||
self.p_max = max(self.p_max, p)
|
self.p_n += 1
|
||||||
|
self.p_sum += float(p)
|
||||||
|
self.p_max = max(self.p_max, float(p))
|
||||||
r = c.n_rays / expected_rays(c.d)
|
r = c.n_rays / expected_rays(c.d)
|
||||||
self.r_sum += r
|
self.r_sum += r
|
||||||
self.r_last = r
|
self.r_last = r
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class DescendingNeurons:
|
||||||
novelty_floor: float = 0.10, max_range: float = 200.0,
|
novelty_floor: float = 0.10, max_range: float = 200.0,
|
||||||
warn_far: float | None = None, warn_far_from: float = 90.0,
|
warn_far: float | None = None, warn_far_from: float = 90.0,
|
||||||
track_blend: float = 0.0, track_score: str = "w_mean",
|
track_blend: float = 0.0, track_score: str = "w_mean",
|
||||||
track_readout=None):
|
track_gate: float = 0.0, track_readout=None):
|
||||||
self.warn_evidence = warn_evidence
|
self.warn_evidence = warn_evidence
|
||||||
self.clear_evidence = clear_evidence
|
self.clear_evidence = clear_evidence
|
||||||
self.emergency_evidence = emergency_evidence
|
self.emergency_evidence = emergency_evidence
|
||||||
|
|
@ -87,6 +87,13 @@ class DescendingNeurons:
|
||||||
# покадрового считывания: 0 — решает только улика, ровно как
|
# покадрового считывания: 0 — решает только улика, ровно как
|
||||||
# раньше, 1 — только модель. Так размен меряется, а не объявляется.
|
# раньше, 1 — только модель. Так размен меряется, а не объявляется.
|
||||||
self.track_blend = track_blend
|
self.track_blend = track_blend
|
||||||
|
# Жёсткий порог по тому же отсчёту — вместо смешивания или вместе с
|
||||||
|
# ним. Разница существенная: смешивание опускает отсчёт И предмету
|
||||||
|
# тоже (обе величины меньше единицы), а порог ничего не отнимает у
|
||||||
|
# того, кто его прошёл. На реальном объекте средний вес 0.998 у
|
||||||
|
# предмета против 0.269 у ложных треков — разделение, под которое
|
||||||
|
# порог и просится. 0 — не проверять.
|
||||||
|
self.track_gate = track_gate
|
||||||
self.track_score = track_score
|
self.track_score = track_score
|
||||||
self.track_readout = track_readout
|
self.track_readout = track_readout
|
||||||
self._latched: set[int] = set()
|
self._latched: set[int] = set()
|
||||||
|
|
@ -120,7 +127,8 @@ class DescendingNeurons:
|
||||||
continue
|
continue
|
||||||
ready.append((t, d))
|
ready.append((t, d))
|
||||||
|
|
||||||
use_track = self.track_blend > 0.0 and bool(ready)
|
use_track = ((self.track_blend > 0.0 or self.track_gate > 0.0)
|
||||||
|
and bool(ready))
|
||||||
if use_track and self.track_score == "model":
|
if use_track and self.track_score == "model":
|
||||||
if self.track_readout is None:
|
if self.track_readout is None:
|
||||||
use_track = False
|
use_track = False
|
||||||
|
|
@ -139,6 +147,10 @@ class DescendingNeurons:
|
||||||
else t.p_mean if self.track_score == "p_mean"
|
else t.p_mean if self.track_score == "p_mean"
|
||||||
else t.w_ema if self.track_score == "w_ema"
|
else t.w_ema if self.track_score == "w_ema"
|
||||||
else t.w_mean)
|
else t.w_mean)
|
||||||
|
if self.track_gate > 0.0 and q < self.track_gate:
|
||||||
|
self._latched.discard(t.id)
|
||||||
|
continue
|
||||||
|
if self.track_blend > 0.0:
|
||||||
b = min(max(self.track_blend, 0.0), 1.0)
|
b = min(max(self.track_blend, 0.0), 1.0)
|
||||||
score = (max(q, 1e-4) ** b
|
score = (max(q, 1e-4) ** b
|
||||||
* max(t.evidence, 1e-4) ** (1.0 - b))
|
* max(t.evidence, 1e-4) ** (1.0 - b))
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ class Params:
|
||||||
# обученного отсчёта с уликой перед сравнением с порогом. 0 — решает
|
# обученного отсчёта с уликой перед сравнением с порогом. 0 — решает
|
||||||
# только улика, как раньше. Замер — EXPERIMENTS п. 15.
|
# только улика, как раньше. Замер — EXPERIMENTS п. 15.
|
||||||
track_blend: float = 0.0
|
track_blend: float = 0.0
|
||||||
|
track_gate: float = 0.0 # жёсткий порог по тому же отсчёту
|
||||||
# Чем считать «насколько это предмет» по треку: `w_mean` — средний вес
|
# Чем считать «насколько это предмет» по треку: `w_mean` — средний вес
|
||||||
# наблюдения за всю жизнь, `w_ema` — он же со скользящим забыванием,
|
# наблюдения за всю жизнь, `w_ema` — он же со скользящим забыванием,
|
||||||
# `p_mean` — средний покадровый отсчёт считывания, `model` — обученное
|
# `p_mean` — средний покадровый отсчёт считывания, `model` — обученное
|
||||||
|
|
@ -225,6 +226,7 @@ class FlyGuard:
|
||||||
warn_far=self.p.warn_far,
|
warn_far=self.p.warn_far,
|
||||||
warn_far_from=self.p.warn_far_from,
|
warn_far_from=self.p.warn_far_from,
|
||||||
track_blend=self.p.track_blend,
|
track_blend=self.p.track_blend,
|
||||||
|
track_gate=self.p.track_gate,
|
||||||
track_score=self.p.track_score,
|
track_score=self.p.track_score,
|
||||||
track_readout=track_readout) if self.p.use_tracking
|
track_readout=track_readout) if self.p.use_tracking
|
||||||
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,24 @@
|
||||||
здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 %
|
здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 %
|
||||||
наблюдений, а до решения доходит 38 % — улика есть, порога не набирает.
|
наблюдений, а до решения доходит 38 % — улика есть, порога не набирает.
|
||||||
|
|
||||||
Поэтому вход здесь другой:
|
Поэтому вход здесь другой: сводки за всю жизнь трека, а не последнее
|
||||||
|
значение, — средний и максимальный вес наблюдения, средний и максимальный
|
||||||
|
покадровый отсчёт MBON, наклон числа лучей по времени, разбросы положения.
|
||||||
|
|
||||||
* `s_std` — разброс места в тоннеле, которое подразумевают наблюдения.
|
**ЧЕМ ЭТО КОНЧИЛОСЬ.** Обученная модель не обгоняет ни один из признаков,
|
||||||
У неподвижного предмета оно постоянно по построению, у ложного трека,
|
которые ей же и скормили: leave-one-bag-out AUC 0.873 против 0.880 у одного
|
||||||
собранного из разных кусков стены, — гуляет. В улику этот признак
|
среднего веса наблюдения и 0.895 у среднего отсчёта считывания. Причина — не в
|
||||||
сейчас не входит вообще, ни одним множителем;
|
схеме: наблюдений 9 299, но это сотни кадров подряд по одному и тому же треку,
|
||||||
* `rays_slope` — растёт ли число лучей так, как обязано расти при
|
и независимых примеров тут сотни, а не тысячи. Поэтому в решении участвует
|
||||||
сближении. У предмета оно идёт по закону решётки, у случайного
|
**не модель, а прямо среднее** (`descending.track_score`, по умолчанию
|
||||||
совпадения — как придётся;
|
`w_mean`), а всё в этом файле — инструмент замера. Разбор — EXPERIMENTS п. 15.
|
||||||
* сводки веса наблюдения и покадрового отсчёта MBON: среднее и максимум
|
|
||||||
за всю жизнь трека, а не последнее значение.
|
|
||||||
|
|
||||||
Схема при этом не меняется: те же клетки Кеньона по шесть когтей, то же
|
Отдельно: `s_std` и `u_std` в решении использовать нельзя. На полигоне разброс
|
||||||
торможение APL, тот же один выход. Это ровно та же операция, что MBON
|
привязки к месту выглядит лучшим признаком дальней полосы (AUC 0.869 за 160 м),
|
||||||
проделывает над кандидатом, только этажом выше — над тем, что центральный
|
а на настоящем объекте он перевёрнут (0.223): вставка ставится на дальность,
|
||||||
комплекс успел накопить. У мухи так же: нисходящие нейроны получают сходящийся
|
посчитанную из той же оценки собственного движения, что двигает мировую
|
||||||
вход и от зрительных путей, и от центрального комплекса, а не только от сетчатки.
|
координату, и разброса у неё нет по построению. Признаки оставлены в
|
||||||
|
дескрипторе нарочно — чтобы проверку можно было повторить (п. 15.4).
|
||||||
Стоимость — одно умножение матрицы на трек, и только для тех треков, что уже
|
|
||||||
прошли дешёвые проверки в `descending.py`.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -55,6 +53,8 @@ TRACK_FEATURES = (
|
||||||
"rays_slope", # и наклон по времени
|
"rays_slope", # и наклон по времени
|
||||||
"w_mean", "w_max", # вес наблюдения
|
"w_mean", "w_max", # вес наблюдения
|
||||||
"p_mean", "p_max", # покадровый отсчёт считывания
|
"p_mean", "p_max", # покадровый отсчёт считывания
|
||||||
|
# Оба разброса — ТОЛЬКО для замеров: на синтетике сильны, на реальном
|
||||||
|
# объекте перевёрнуты. См. шапку файла и EXPERIMENTS п. 15.4.
|
||||||
"s_std", # разброс места в тоннеле, м
|
"s_std", # разброс места в тоннеле, м
|
||||||
"u_std", # разброс поперечного положения, м
|
"u_std", # разброс поперечного положения, м
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,29 @@ def test_track_accumulators_do_not_touch_the_evidence():
|
||||||
assert abs(t.evidence - cx.gain * t.w_mean) < 1e-6
|
assert abs(t.evidence - cx.gain * t.w_mean) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_p_mean_falls_back_when_there_is_no_readout():
|
||||||
|
"""Без модели покадрового считывания отсчёта нет, и ноль тут погасил бы всё."""
|
||||||
|
from flyguard.central_complex import Track
|
||||||
|
|
||||||
|
t = Track(id=1, s_world=60.0, u=0.0, h=0.3, width=0.5, height=1.7,
|
||||||
|
n_obs=4, w_sum=4 * 0.8)
|
||||||
|
assert t.p_n == 0
|
||||||
|
assert abs(t.p_mean - 0.8) < 1e-9
|
||||||
|
t.p_n, t.p_sum = 4, 4 * 0.3
|
||||||
|
assert abs(t.p_mean - 0.3) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_gate_removes_the_weak_track_without_touching_the_strong_one():
|
||||||
|
"""Порог ничего не отнимает у прошедшего — в отличие от смешивания."""
|
||||||
|
from flyguard.descending import DescendingNeurons
|
||||||
|
|
||||||
|
out = DescendingNeurons(track_gate=0.5, track_score="w_mean").decide(
|
||||||
|
_pair_of_tracks(), speed=10.0)
|
||||||
|
assert {o.track_id for o in out.objects} == {1}
|
||||||
|
assert out.objects[0].confidence == pytest.approx(
|
||||||
|
DescendingNeurons().decide(_pair_of_tracks(), speed=10.0).objects[0].confidence)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ улучшения габарита и решения
|
# ------------------------------------------------------------ улучшения габарита и решения
|
||||||
|
|
||||||
class _MockTrackFrame:
|
class _MockTrackFrame:
|
||||||
|
|
@ -681,4 +704,3 @@ def test_benchmark_augmentation_logic():
|
||||||
assert sc.name == "человек_лежа"
|
assert sc.name == "человек_лежа"
|
||||||
assert sc.v_lat == 0.15
|
assert sc.v_lat == 0.15
|
||||||
assert not sc.is_static
|
assert not sc.is_static
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ def main() -> None:
|
||||||
help="каталог с моделями по складкам (track_<бэг>.npz)")
|
help="каталог с моделями по складкам (track_<бэг>.npz)")
|
||||||
ap.add_argument("--track-blend", type=float, default=None,
|
ap.add_argument("--track-blend", type=float, default=None,
|
||||||
help="доля считывания по треку в решении; 0 — только улика")
|
help="доля считывания по треку в решении; 0 — только улика")
|
||||||
|
ap.add_argument("--track-gate", type=float, default=None,
|
||||||
|
help="жёсткий порог по отсчёту трека; 0 — не проверять")
|
||||||
ap.add_argument("--track-score", default=None,
|
ap.add_argument("--track-score", default=None,
|
||||||
choices=("w_mean", "w_ema", "p_mean", "model"),
|
choices=("w_mean", "w_ema", "p_mean", "model"),
|
||||||
help="чем мерить трек: средний вес наблюдения, средний "
|
help="чем мерить трек: средний вес наблюдения, средний "
|
||||||
|
|
@ -231,6 +233,8 @@ def main() -> None:
|
||||||
over["track_blend"] = args.track_blend
|
over["track_blend"] = args.track_blend
|
||||||
if args.track_score is not None:
|
if args.track_score is not None:
|
||||||
over["track_score"] = args.track_score
|
over["track_score"] = args.track_score
|
||||||
|
if args.track_gate is not None:
|
||||||
|
over["track_gate"] = args.track_gate
|
||||||
params = Params(**over)
|
params = Params(**over)
|
||||||
fold_paths: dict[str, str] = {}
|
fold_paths: dict[str, str] = {}
|
||||||
if args.mbon_dir:
|
if args.mbon_dir:
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,8 @@ def main() -> None:
|
||||||
help="каталог с моделями по складкам (track_<бэг>.npz)")
|
help="каталог с моделями по складкам (track_<бэг>.npz)")
|
||||||
ap.add_argument("--track-blend", type=float, default=None,
|
ap.add_argument("--track-blend", type=float, default=None,
|
||||||
help="доля считывания по треку в решении; 0 — только улика")
|
help="доля считывания по треку в решении; 0 — только улика")
|
||||||
|
ap.add_argument("--track-gate", type=float, default=None,
|
||||||
|
help="жёсткий порог по отсчёту трека; 0 — не проверять")
|
||||||
ap.add_argument("--track-score", default=None,
|
ap.add_argument("--track-score", default=None,
|
||||||
choices=("w_mean", "w_ema", "p_mean", "model"),
|
choices=("w_mean", "w_ema", "p_mean", "model"),
|
||||||
help="чем мерить трек: средний вес наблюдения, средний "
|
help="чем мерить трек: средний вес наблюдения, средний "
|
||||||
|
|
@ -346,6 +348,8 @@ def main() -> None:
|
||||||
over["track_blend"] = args.track_blend
|
over["track_blend"] = args.track_blend
|
||||||
if args.track_score is not None:
|
if args.track_score is not None:
|
||||||
over["track_score"] = args.track_score
|
over["track_score"] = args.track_score
|
||||||
|
if args.track_gate is not None:
|
||||||
|
over["track_gate"] = args.track_gate
|
||||||
params = Params(**over)
|
params = Params(**over)
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue