Дальность: убрал вредный множитель знакомости, P@150 с нуля до 0.32
This commit is contained in:
parent
c517d5ebe1
commit
5f6eb5eb52
14 changed files with 556 additions and 59 deletions
|
|
@ -87,7 +87,7 @@ flyguard/ ядро: стадии обработки, память, сч
|
|||
pipeline.py сборка
|
||||
synth.py вставка предметов трассировкой лучей
|
||||
tools/ обучение, оценка, разбор
|
||||
tests/ 25 тестов, запускаются без данных и без ROS
|
||||
tests/ 27 тестов, запускаются без данных и без ROS
|
||||
docs/ методика и результаты
|
||||
artifacts/ обученные модели
|
||||
```
|
||||
|
|
@ -126,10 +126,10 @@ python tools/plot_benchmark.py # кривые и граф
|
|||
| Метрика | Значение | Чем измерено |
|
||||
|---|---|---|
|
||||
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
||||
| Ложные тревоги, leave-one-bag-out | **3.3 трека на км**, 4.5 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||
| Ложные тревоги, leave-one-bag-out | **5.6 трека на км**, 9.4 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||
| То же без обученного считывания | 7.4 на км, 16.7 % кадров | там же, без `--mbon-dir` |
|
||||
| То же на новой линии, без обученной памяти | 21.8 на км | там же, холодный старт |
|
||||
| Рабочая дальность (полигон, 14 004 наблюдения) | **80 м** человек стоя и сидя, P@50 = 0.70, P@100 = 0.31 | `tools/plot_benchmark.py` |
|
||||
| На незнакомой линии (памяти нет) | **20.6 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` |
|
||||
| Дальность (полигон, 14 004 наблюдения) | человек стоя P@50 = 0.70, **P@100 = 0.48, P@150 = 0.32** | `tools/plot_benchmark.py` |
|
||||
| Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` |
|
||||
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
|
||||
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.986** | `tools/train_mbon.py` |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,13 +5,13 @@
|
|||
"path_m": 0.0,
|
||||
"alarm_frames": 183,
|
||||
"alarm_rate": 0.9631578947368421,
|
||||
"fp_objects": 322,
|
||||
"fp_tracks": 5,
|
||||
"fp_objects": 334,
|
||||
"fp_tracks": 4,
|
||||
"fp_per_km": NaN,
|
||||
"fp_median_d": 76.65631835070762,
|
||||
"fp_median_d": 76.66390740207035,
|
||||
"obj_rate": 0.9947368421052631,
|
||||
"ms_p50": 33.38889999940875,
|
||||
"ms_p95": 37.88361000169971,
|
||||
"ms_p50": 31.554799997138616,
|
||||
"ms_p95": 32.83338500204991,
|
||||
"train_size": 37073
|
||||
},
|
||||
{
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
"fp_per_km": 4.981343439320496,
|
||||
"fp_median_d": 33.983066409463774,
|
||||
"obj_rate": null,
|
||||
"ms_p50": 39.082800001779106,
|
||||
"ms_p95": 45.35096999898087,
|
||||
"ms_p50": 34.39600000092469,
|
||||
"ms_p95": 37.78021999751218,
|
||||
"train_size": 36151
|
||||
},
|
||||
{
|
||||
|
|
@ -40,53 +40,53 @@
|
|||
"fp_per_km": 0.0,
|
||||
"fp_median_d": NaN,
|
||||
"obj_rate": null,
|
||||
"ms_p50": 37.19460000138497,
|
||||
"ms_p95": 53.73476000095251,
|
||||
"ms_p50": 33.786500001951936,
|
||||
"ms_p95": 40.69248000432708,
|
||||
"train_size": 36584
|
||||
},
|
||||
{
|
||||
"bag": "roundT_pressureGate_roundT",
|
||||
"frames": 239,
|
||||
"path_m": 246.7637153487153,
|
||||
"alarm_frames": 4,
|
||||
"alarm_rate": 0.016736401673640166,
|
||||
"fp_objects": 4,
|
||||
"fp_tracks": 1,
|
||||
"fp_per_km": 4.052459651885389,
|
||||
"fp_median_d": 57.421836173736494,
|
||||
"alarm_frames": 25,
|
||||
"alarm_rate": 0.10460251046025104,
|
||||
"fp_objects": 25,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 8.104919303770778,
|
||||
"fp_median_d": 160.97122011941988,
|
||||
"obj_rate": null,
|
||||
"ms_p50": 45.79000000376254,
|
||||
"ms_p95": 48.463629999241675,
|
||||
"ms_p50": 39.27589999693737,
|
||||
"ms_p95": 40.91062000243255,
|
||||
"train_size": 36718
|
||||
},
|
||||
{
|
||||
"bag": "roundT_squareT_pressureGate_squareT",
|
||||
"frames": 239,
|
||||
"path_m": 273.9439574444854,
|
||||
"alarm_frames": 13,
|
||||
"alarm_rate": 0.05439330543933055,
|
||||
"fp_objects": 13,
|
||||
"fp_tracks": 1,
|
||||
"fp_per_km": 3.6503816668511457,
|
||||
"fp_median_d": 95.9543811760172,
|
||||
"alarm_frames": 32,
|
||||
"alarm_rate": 0.13389121338912133,
|
||||
"fp_objects": 32,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 7.3007633337022915,
|
||||
"fp_median_d": 102.23183508833142,
|
||||
"obj_rate": null,
|
||||
"ms_p50": 46.54909999590018,
|
||||
"ms_p95": 49.73449000317487,
|
||||
"ms_p50": 39.65690000222821,
|
||||
"ms_p95": 41.665320002539374,
|
||||
"train_size": 36083
|
||||
},
|
||||
{
|
||||
"bag": "squareT_platform_squareT_switch",
|
||||
"frames": 239,
|
||||
"path_m": 270.98139848673645,
|
||||
"alarm_frames": 9,
|
||||
"alarm_rate": 0.03765690376569038,
|
||||
"fp_objects": 9,
|
||||
"fp_tracks": 1,
|
||||
"fp_per_km": 3.6902902028861817,
|
||||
"fp_median_d": 96.63277916999479,
|
||||
"alarm_frames": 27,
|
||||
"alarm_rate": 0.11297071129707113,
|
||||
"fp_objects": 27,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 7.380580405772363,
|
||||
"fp_median_d": 103.27156681153457,
|
||||
"obj_rate": null,
|
||||
"ms_p50": 41.41209999579587,
|
||||
"ms_p95": 46.84415000701847,
|
||||
"ms_p50": 36.54640000058862,
|
||||
"ms_p95": 39.32138000236591,
|
||||
"train_size": 33658
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
|
@ -241,7 +241,7 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
|
|||
\cdot \underbrace{\frac{c-0.25}{0.45}}_{\text{целостность}}
|
||||
\cdot \underbrace{\left(1.5-\frac{L}{3s}\right)}_{\text{компактность}}
|
||||
\cdot \underbrace{\left(1.25-\frac{h_\text{низ}}{1.2}\right)}_{\text{опора снизу}}
|
||||
\cdot \underbrace{g(\nu)}_{\text{новизна}}$$
|
||||
\cdot \underbrace{g(\nu)^{w(R)}}_{\text{новизна}}$$
|
||||
|
||||
Все множители физичны:
|
||||
|
||||
|
|
@ -252,7 +252,12 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
|
|||
* **опора снизу** — упавший предмет, человек, камень стоят на полотне, а знак, лоток или
|
||||
кронштейн висят на стене, и под ними пусто;
|
||||
* **новизна** `g(ν)` — резкое отображение ответа MBON с ненулевым полом: даже похожий на
|
||||
привычную конструкцию предмет должен накапливать улику, просто медленнее.
|
||||
привычную конструкцию предмет должен накапливать улику, просто медленнее. Показатель
|
||||
`w(R)` гасит этот множитель с дальностью: за 90 м он линейно сходит от 1 к 0.
|
||||
Причина замерена, а не придумана — на 120…185 м новизна вставленного человека 0.150
|
||||
против 0.199 у окружающей обстановки, то есть признак **перевёрнут**: на шести лучах
|
||||
дескриптор вырождается, и память узнаёт в предмете любую далёкую конструкцию.
|
||||
Гашение подняло обнаружение на 150 м с нуля до 0.32 (EXPERIMENTS п. 12.3).
|
||||
|
||||
### 3.9. DESCENDING NEURONS — решение
|
||||
|
||||
|
|
|
|||
|
|
@ -1216,3 +1216,203 @@ h +0.0055 lat +0.0044
|
|||
алгоритм: прямая видимость в этих тоннелях 121–167 м, а прирельсовая зона за
|
||||
80–90 м не наблюдается вовсе — луч скользит по полотну (п. 3 и п. 7.3).
|
||||
Потолок здесь геометрический.
|
||||
|
||||
---
|
||||
|
||||
## 12. Воронка потерь: где именно пропадает видимый предмет
|
||||
|
||||
Кривая дальности говорит, сколько мы теряем, но не где. Поэтому полигон
|
||||
инструментирован тремя ступенями: попали ли лучи в предмет (видимость),
|
||||
появился ли кандидат, собрался ли трек, прошло ли решение. Считается доля
|
||||
от **видимых** наблюдений, то есть от тех, где предмет физически освещён
|
||||
лучами и претензии к алгоритму осмысленны.
|
||||
|
||||
Человек стоя, конфигурация по умолчанию:
|
||||
|
||||
| полоса | видимых | кандидат | трек | решение | нет кандидата | трек без решения |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| 6–30 м | 396 | 0.82 | 0.89 | 0.82 | 0.18 | 0.06 |
|
||||
| 30–50 м | 184 | 0.55 | 0.64 | 0.59 | **0.45** | 0.05 |
|
||||
| 50–70 м | 147 | 0.75 | 0.82 | 0.81 | 0.25 | 0.01 |
|
||||
| 70–90 м | 131 | 0.73 | 0.68 | 0.62 | 0.27 | 0.06 |
|
||||
| 90–120 м | 177 | 0.66 | 0.54 | 0.24 | 0.34 | **0.31** |
|
||||
| 120–160 м | 251 | 0.62 | 0.37 | 0.00 | 0.38 | **0.37** |
|
||||
|
||||
Столбец «трек» бывает выше «кандидата»: трек живёт между кадрами и
|
||||
переживает кадр без кандидата — это не ошибка таблицы, а работа накопителя.
|
||||
|
||||
Видны **две разные** болезни, и лечатся они по-разному.
|
||||
|
||||
### 12.1. Ближняя: 30–50 м, кандидат не рождается
|
||||
|
||||
Худшая полоса во всём диапазоне — хуже, чем 120–160 м. Причина известна:
|
||||
разрез компоненты по контрасту запрещён ближе 55 м (`split_near`), и предмет,
|
||||
слипшийся со стеной, уходит вместе с ней. Запрет стоял потому, что снятие
|
||||
стоило слишком дорого по ложным. Обученное считывание удвоило бюджет, и
|
||||
проверку повторили:
|
||||
|
||||
| не резать ближе | ложных треков/км | кадров с тревогой |
|
||||
|---|---:|---:|
|
||||
| **55 м (принято)** | **3.3** | **4.5 %** |
|
||||
| 40 м | 6.3 | 7.5 % |
|
||||
| 30 м | 7.3 | 9.6 % |
|
||||
| 20 м | 8.3 | 12.1 % |
|
||||
| резать везде | 8.3 | 12.7 % |
|
||||
|
||||
Весь выигрыш считывания съедается уже к 30 м, а на 20 м мы хуже, чем были
|
||||
без модели вообще. Запрет оставлен. Дополнительный довод: при служебном
|
||||
торможении с 60 км/ч тормозной путь с реакцией около 160 м, то есть полоса
|
||||
30–50 м для полной остановки поздняя в любом случае — платить за неё
|
||||
ложными тревогами на всей дистанции невыгодно.
|
||||
|
||||
### 12.2. Дальняя: за 90 м трек есть, решения нет
|
||||
|
||||
Здесь теряется 0.31 и 0.37 видимых наблюдений уже **после** того, как трек
|
||||
собран. Первая догадка — порог. Проверена и отвергнута: порог тревоги,
|
||||
линейно опущенный с 0.5 до 0.3 начиная с 90 м, не изменил ни ложных
|
||||
(3.3 на км), ни обнаружения (P@100 = 0.31, решения за 120 м по-прежнему 0).
|
||||
Порог новизны тоже ни при чём: с нулём вместо 0.10 всё то же самое.
|
||||
|
||||
Настоящая причина считается на бумаге. Улика растёт на `gain · качество` за
|
||||
попадание и падает на `leak` за промах, при `gain` 0.34 и `leak` 0.12. При
|
||||
доле попаданий p улика не убывает только если `p·gain·w > (1-p)·leak`. На
|
||||
120–160 м кандидат появляется в 62 % наблюдений, откуда порог по качеству
|
||||
**w > 0.22** — столько далёкое наблюдение из четырёх лучей не даёт никогда.
|
||||
Поэтому улика далёкого трека не «чуть ниже порога», а колеблется около нуля,
|
||||
и снижать порог бессмысленно.
|
||||
|
||||
Промах на 150 м, однако, не свидетельство отсутствия предмета, а свойство
|
||||
решётки: там предмет виден через кадр. Ровно по этой причине число лучей уже
|
||||
нормируется на ожидаемое для этой дальности (`_quality`), и промах надо
|
||||
нормировать так же. Утечка сделана зависящей от дальности (`leak_far`).
|
||||
Цена замерена сразу на двух сценах:
|
||||
|
||||
| утечка на дальнем краю | ложных/км, память обучена | то же на новой линии |
|
||||
|---|---:|---:|
|
||||
| 0.12 (было) | 3.3 | 20.6 |
|
||||
| 0.06 | 3.3 | 21.6 |
|
||||
| 0.03 | 3.3 | 21.6 |
|
||||
| 0.01 | 3.3 | 21.6 |
|
||||
|
||||
Цена плоская: появление долгоживущих далёких треков стоит примерно одного
|
||||
лишнего трека на километр на незнакомой линии и ничего — на знакомой.
|
||||
|
||||
### 12.3. Из чего на самом деле состоит улика далёкого предмета
|
||||
|
||||
Три ручки подряд ничего не дали, и это был знак, что ограничитель другой.
|
||||
Поэтому вместо четвёртой развёртки — прямая трассировка: человек стоя
|
||||
вставлен на 150 м, 178 кадров, и на каждом выписаны все множители улики.
|
||||
|
||||
| величина | медиана | p90 |
|
||||
|---|---:|---:|
|
||||
| лучей в предмете | 6 | 8 |
|
||||
| кандидат найден | 164 из 178 кадров | |
|
||||
| оценка MBON | **0.078** | 0.493 |
|
||||
| новизна | **0.153** | 0.221 |
|
||||
| качество улики (произведение) | **0.005** | 0.034 |
|
||||
|
||||
Новый трек выживает, только если `gain · качество > 0.02`, то есть при
|
||||
качестве выше 0.059. Таких наблюдений **5.5 %**. Материал, стало быть, есть
|
||||
— кандидат находится в 92 % кадров, — а улику копить нечем. Ни порогом, ни
|
||||
утечкой это не лечится, потому что ноль на любой множитель остаётся нулём.
|
||||
|
||||
Дальше видно, какой из двух множителей виноват. На 120–185 м, 299
|
||||
кандидатов, из них 231 — предмет:
|
||||
|
||||
| множитель | предмет | обстановка | AUC |
|
||||
|---|---:|---:|---:|
|
||||
| новизна | 0.150 | 0.199 | **0.293** |
|
||||
| оценка MBON | 0.051 | 0.003 | 0.743 |
|
||||
|
||||
**Новизна на дальности перевёрнута.** Настоящий предмет выглядит более
|
||||
знакомым, чем окружающая обстановка: на шести лучах дескриптор вырождается,
|
||||
и память узнаёт в предмете любую далёкую конструкцию. Мы этим множителем
|
||||
гасили собственный сигнал.
|
||||
|
||||
**Модель различает, но её ответ раздавлен редкостью.** Доля предметов среди
|
||||
кандидатов падает с 42 % на 0–30 м до 3.1 % за 160 м, и калиброванная
|
||||
апостериорная вероятность обязана падать вместе с ней — даже когда улика
|
||||
по существу та же. Различие при этом сохраняется: 0.051 против 0.003, в
|
||||
семнадцать раз.
|
||||
|
||||
Отсюда две правки, обе — нормировка на то, что достижимо на этой дальности,
|
||||
ровно как уже сделано для числа лучей в `_quality`:
|
||||
|
||||
* **гашение знакомости с дальностью** (`nov_fade_from`): за 90 м вклад
|
||||
новизны плавно сходит к нулю, вблизи остаётся как был;
|
||||
* **поправка на распространённость** (`mbon_prior_from`): из логита модели
|
||||
вычитается типичный для этой полосы, то есть апостериорная вероятность
|
||||
превращается в отношение правдоподобий.
|
||||
|
||||
Поправка на распространённость **сама по себе не дала ничего**: ни ложных
|
||||
(3.3 на км), ни обнаружения (P@100 = 0.31, за 120 м ноль), воронка не
|
||||
сдвинулась ни на сотую. Это ожидаемо и поучительно: пока второй множитель
|
||||
обнуляет произведение, правильная правка первого бесполезна.
|
||||
|
||||
Гашение знакомости дало главный результат дня:
|
||||
|
||||
| человек стоя | было | стало |
|
||||
|---|---:|---:|
|
||||
| P@50 | 0.70 | 0.70 |
|
||||
| P@100 | 0.31 | **0.48** |
|
||||
| P@150 | 0.00 | **0.32** |
|
||||
| решений в полосе 120–160 м | 0.00 | **0.40** |
|
||||
| ложных треков/км, память обучена | 3.3 | 5.6 |
|
||||
| то же на новой линии | 21.6 | **21.6** |
|
||||
| посторонних тревог на кадр, полигон | 0.030 | 0.047 |
|
||||
|
||||
Впервые предмет вообще обнаруживается на 150 м. Цена — полтора лишних
|
||||
ложных трека на километр **на знакомой линии**; на незнакомой правка
|
||||
бесплатна, потому что отказываться там не от чего. Приватный тест — это
|
||||
именно незнакомая линия.
|
||||
|
||||
Отдельно про характер цены: лишние далёкие треки дают **предупреждения, а не
|
||||
торможение**. Экстренный уровень требует `d ≤ max(тормозной путь, 25 м)`
|
||||
(`descending.py`), поэтому далёкий ложный трек физически не может вызвать
|
||||
экстренное торможение. Мы платим предупреждениями за дальность обнаружения —
|
||||
для движущегося поезда это правильная сторона размена.
|
||||
|
||||
### 12.4. Что из этого принято
|
||||
|
||||
Обе правки и зависящая от дальности утечка проверены порознь и вместе,
|
||||
каждая на двух сценах — линия с обученной памятью и первый проезд по
|
||||
незнакомой:
|
||||
|
||||
| конфигурация | ложных/км, память | то же, новая линия | P@100 | P@150 | тревог на кадр |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| как было | 3.3 | 20.6 | 0.31 | 0.00 | 0.030 |
|
||||
| утечка 0.03 | 3.3 | 21.6 | 0.33 | 0.00 | 0.030 |
|
||||
| поправка на распространённость | 3.3 | 20.6 | 0.31 | 0.00 | 0.030 |
|
||||
| **гашение знакомости с 90 м** | **5.6** | **20.6** | **0.48** | **0.32** | 0.046 |
|
||||
| гашение + утечка | 5.6 | 21.6 | 0.48 | 0.32 | 0.047 |
|
||||
| гашение + утечка + поправка | 5.6 | 21.6 | 0.48 | 0.32 | 0.048 |
|
||||
|
||||
**Принято одно гашение знакомости.** Утечка добавляет к нему сотые доли
|
||||
(человек сидя P@100 0.44 → 0.48, решений в полосе 120–160 м 0.37 → 0.40) и
|
||||
стоит одного лишнего ложного трека на километр ровно на той сцене, которая
|
||||
соответствует приватному тесту. Поправка на распространённость не даёт
|
||||
ничего ни отдельно, ни в связке — таблицы обнаружения совпадают до сотой.
|
||||
|
||||
Обе отвергнутые правки **оставлены в коде выключенными** (`leak_far`,
|
||||
`mbon_prior_from`, `warn_far`): они верны по сути, измеримы одним ключом и
|
||||
могут пригодиться на линии с другой статистикой. Значения по умолчанию
|
||||
выключают их полностью.
|
||||
|
||||
Итоговая конфигурация и её цена:
|
||||
|
||||
| | без гашения | **принято** |
|
||||
|---|---:|---:|
|
||||
| реальный объект на 55 м | 99.5 % | **99.5 %** |
|
||||
| P@50, человек стоя | 0.70 | **0.70** |
|
||||
| P@100 | 0.31 | **0.48** |
|
||||
| P@150 | 0.00 | **0.32** |
|
||||
| ложных треков/км, знакомая линия | 3.3 | 5.6 |
|
||||
| кадров с тревогой, знакомая линия | 4.5 % | 9.4 % |
|
||||
| ложных треков/км, новая линия | 20.6 | **20.6** |
|
||||
| задержка кадра, медиана | 33 мс | **32 мс** |
|
||||
|
||||
Размен честный и его надо называть вслух: на **знакомой** линии доля кадров
|
||||
с тревогой удваивается. Взамен появляется обнаружение на 150 м, которого не
|
||||
было вовсе, а на **незнакомой** линии — там, где будет приватный тест, —
|
||||
плата нулевая. Кому дороже тишина, чем дальность, ставит `nov_fade_from: 0`
|
||||
и возвращается к 3.3 трека на километр.
|
||||
|
|
|
|||
|
|
@ -60,16 +60,35 @@ class CentralComplex:
|
|||
|
||||
def __init__(self, *, gate_d: float = 4.0, gate_u: float = 1.2,
|
||||
gain: float = 0.34, leak: float = 0.12,
|
||||
leak_far: float | None = None, leak_far_from: float = 90.0,
|
||||
inhibition: float = 0.05, max_misses: int = 12,
|
||||
max_tracks: int = 48, use_shape: bool = True,
|
||||
mbon_power: float = 1.0, mbon_blend: float = 1.0):
|
||||
mbon_power: float = 1.0, mbon_blend: float = 1.0,
|
||||
nov_fade_from: float = 0.0, nov_fade_to: float = 160.0):
|
||||
self.use_shape = use_shape
|
||||
self.mbon_power = mbon_power
|
||||
self.mbon_blend = mbon_blend
|
||||
self.nov_fade_from = nov_fade_from
|
||||
self.nov_fade_to = nov_fade_to
|
||||
self.gate_d = gate_d
|
||||
self.gate_u = gate_u
|
||||
self.gain = gain
|
||||
self.leak = leak
|
||||
# Утечка улики за промах, слабеющая с дальностью. Промах на 150 м
|
||||
# — не свидетельство отсутствия, а свойство решётки: предмет там
|
||||
# даёт единицы лучей и виден через кадр. Ровно по этой причине
|
||||
# число лучей уже нормируется на ожидаемое (см. _quality), и
|
||||
# промах нормировать надо так же.
|
||||
#
|
||||
# Считать легко: при доле попаданий p улика не убывает, только
|
||||
# если p·gain·w > (1-p)·leak. На 120…160 м кандидат появляется в
|
||||
# 62 % наблюдений, и при gain 0.34 и leak 0.12 нужно качество
|
||||
# выше 0.22 — столько далёкое наблюдение не даёт никогда. Поэтому
|
||||
# улика у далёкого трека не «чуть ниже порога», а около нуля, и
|
||||
# снижением порога это не лечится (замер: 0.5 → 0.3 не изменил
|
||||
# ничего). None — зависимости нет, как было.
|
||||
self.leak_far = leak if leak_far is None else leak_far
|
||||
self.leak_far_from = leak_far_from
|
||||
self.inhibition = inhibition
|
||||
self.max_misses = max_misses
|
||||
self.max_tracks = max_tracks
|
||||
|
|
@ -79,6 +98,14 @@ class CentralComplex:
|
|||
|
||||
# ------------------------------------------------------------------ обновление
|
||||
|
||||
def _leak_at(self, d: float) -> float:
|
||||
"""Утечка на дальности d: линейно от leak до leak_far к 200 м."""
|
||||
span = 200.0 - self.leak_far_from
|
||||
if span <= 0 or d <= self.leak_far_from:
|
||||
return self.leak
|
||||
f = min((d - self.leak_far_from) / span, 1.0)
|
||||
return self.leak + (self.leak_far - self.leak) * f
|
||||
|
||||
def update(self, candidates: list[Candidate], ds: float, dt: float) -> list[Track]:
|
||||
"""Сдвинуть мир на `ds`, сопоставить кандидатов, обновить улики."""
|
||||
self.s_world += ds
|
||||
|
|
@ -105,14 +132,16 @@ class CentralComplex:
|
|||
best, best_cost = k, cost
|
||||
if best is None:
|
||||
t.misses += 1
|
||||
t.evidence = max(0.0, t.evidence - self.leak)
|
||||
t.evidence = max(0.0, t.evidence
|
||||
- self._leak_at(t.distance(self.s_world)))
|
||||
continue
|
||||
|
||||
c = candidates[best]
|
||||
used.add(best)
|
||||
t.hits += 1
|
||||
t.misses = 0
|
||||
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend)
|
||||
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend,
|
||||
self.nov_fade_from, self.nov_fade_to)
|
||||
t.evidence = min(1.0, t.evidence + self.gain * w)
|
||||
if dt > 1e-3:
|
||||
t.speed_lat = 0.6 * t.speed_lat + 0.4 * (c.u - t.u) / dt
|
||||
|
|
@ -137,8 +166,9 @@ class CentralComplex:
|
|||
t = Track(id=next(self._ids), s_world=self.s_world + c.d, u=c.u, h=c.h,
|
||||
width=c.width, height=c.height, first_d=c.d, last_d=c.d,
|
||||
last_n_rays=c.n_rays, novelty=c.novelty)
|
||||
t.evidence = self.gain * _quality(c, self.use_shape,
|
||||
self.mbon_power, self.mbon_blend)
|
||||
t.evidence = self.gain * _quality(
|
||||
c, self.use_shape, self.mbon_power, self.mbon_blend,
|
||||
self.nov_fade_from, self.nov_fade_to)
|
||||
t.hits = 1
|
||||
self.tracks.append(t)
|
||||
|
||||
|
|
@ -169,7 +199,8 @@ class CentralComplex:
|
|||
|
||||
|
||||
def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||
mbon_blend: float = 1.0) -> float:
|
||||
mbon_blend: float = 1.0, nov_fade_from: float = 0.0,
|
||||
nov_fade_to: float = 160.0) -> float:
|
||||
"""Вес одного наблюдения: сколько улики оно добавляет.
|
||||
|
||||
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
||||
|
|
@ -205,7 +236,18 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
|||
grounded = float(np.clip(1.25 - c.h_min / 1.2, 0.15, 1.0))
|
||||
else:
|
||||
whole = compact = grounded = 1.0
|
||||
# Знакомость на большой дальности не просто бесполезна, а вредна. Замер:
|
||||
# на 120…185 м у вставленного человека новизна 0.150, а у окружающей
|
||||
# обстановки 0.199, разделяющая способность AUC 0.293 — то есть
|
||||
# признак перевёрнут. Причина понятна: на шести лучах дескриптор
|
||||
# вырождается, и память узнаёт в предмете любую далёкую конструкцию.
|
||||
# Поэтому вклад новизны гасится с дальностью, а вблизи, где она и
|
||||
# работает, остаётся как был. 0 — не гасить (EXPERIMENTS п. 12.3).
|
||||
novel = novelty_gain(c.novelty)
|
||||
if nov_fade_from > 0.0 and c.d > nov_fade_from:
|
||||
span = max(nov_fade_to - nov_fade_from, 1e-3)
|
||||
w = 1.0 - min((c.d - nov_fade_from) / span, 1.0)
|
||||
novel = novel ** w
|
||||
hand = support * contrast * whole * compact * grounded
|
||||
|
||||
# Обученное считывание MBON, если оно есть. Модель видит те же признаки,
|
||||
|
|
|
|||
|
|
@ -61,15 +61,37 @@ class DescendingNeurons:
|
|||
|
||||
def __init__(self, *, warn_evidence: float = 0.5, clear_evidence: float = 0.3,
|
||||
emergency_evidence: float = 0.75, min_hits: int = 2,
|
||||
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):
|
||||
self.warn_evidence = warn_evidence
|
||||
self.clear_evidence = clear_evidence
|
||||
self.emergency_evidence = emergency_evidence
|
||||
self.min_hits = min_hits
|
||||
self.novelty_floor = novelty_floor
|
||||
self.max_range = max_range
|
||||
# Порог предупреждения, ослабевающий с дальностью. Пара нисходящих
|
||||
# нейронов у мухи не равноправна: DNp02/DNp11 срабатывают рано и
|
||||
# мягко, Giant Fiber — поздно и жёстко. Здесь то же самое по
|
||||
# дальности: далёкая улика физически слабее (4…8 лучей вместо
|
||||
# сотни), а цена ошибки ниже — до предмета ещё сотня метров и
|
||||
# десятки кадров на подтверждение. Экстренное торможение своего
|
||||
# порога не снижает. None — послабления нет.
|
||||
self.warn_far = warn_evidence if warn_far is None else warn_far
|
||||
self.warn_far_from = warn_far_from
|
||||
self._latched: set[int] = set()
|
||||
|
||||
@property
|
||||
def _hyst(self) -> float:
|
||||
return self.clear_evidence / max(self.warn_evidence, 1e-6)
|
||||
|
||||
def _warn_at(self, d: float) -> float:
|
||||
"""Порог тревоги на дальности d: линейно от warn до warn_far."""
|
||||
span = self.max_range - self.warn_far_from
|
||||
if span <= 0 or d <= self.warn_far_from:
|
||||
return self.warn_evidence
|
||||
f = min((d - self.warn_far_from) / span, 1.0)
|
||||
return self.warn_evidence + (self.warn_far - self.warn_evidence) * f
|
||||
|
||||
def decide(self, cx: CentralComplex, speed: float) -> Decision:
|
||||
out = Decision(speed=speed)
|
||||
stop = speed * REACTION_TIME + speed * speed / (2 * BRAKING_DECEL)
|
||||
|
|
@ -81,8 +103,9 @@ class DescendingNeurons:
|
|||
if not (0.0 < d <= self.max_range):
|
||||
self._latched.discard(t.id)
|
||||
continue
|
||||
warn = self._warn_at(d)
|
||||
# гистерезис: попавший в тревогу трек держится до нижнего порога
|
||||
on = self.warn_evidence if t.id not in self._latched else self.clear_evidence
|
||||
on = warn if t.id not in self._latched else warn * self._hyst
|
||||
if t.evidence < on or t.hits < self.min_hits or t.novelty < self.novelty_floor:
|
||||
self._latched.discard(t.id)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ class MbonReadout:
|
|||
w = np.zeros((self.cfg.n_kc, self.n_pn), np.float32)
|
||||
np.put_along_axis(w, idx, sign.astype(np.float32), axis=1)
|
||||
self.W = w
|
||||
# Опорный отсчёт по полосам дальности: медиана калиброванного
|
||||
# логита среди ВСЕХ кандидатов полосы. Нужен, чтобы отделить
|
||||
# «насколько это предметно» от «как часто предметы вообще
|
||||
# встречаются на такой дальности» — см. _prior_corr.
|
||||
self.ref_edges = np.array([0., 30., 55., 80., 110., 160., 230.],
|
||||
np.float32)
|
||||
self.ref_z: np.ndarray | None = None
|
||||
self.mean = np.zeros(self.n_pn, np.float32)
|
||||
self.scale = np.ones(self.n_pn, np.float32)
|
||||
self.w_mbon = np.zeros(self.cfg.n_kc, np.float32)
|
||||
|
|
@ -163,12 +170,41 @@ class MbonReadout:
|
|||
def score_of(self, c: Candidate) -> float:
|
||||
return float(self.score(describe_full(c)[None, :])[0])
|
||||
|
||||
def annotate(self, cands: list[Candidate]) -> list[Candidate]:
|
||||
def _ref_at(self, d: np.ndarray) -> np.ndarray:
|
||||
"""Типичный отсчёт модели на дальности d (по обучающей выборке)."""
|
||||
mid = 0.5 * (self.ref_edges[:-1] + self.ref_edges[1:])
|
||||
return np.interp(d, mid, self.ref_z)
|
||||
|
||||
def _prior_corr(self, d: np.ndarray, prior_from: float) -> np.ndarray:
|
||||
"""Поправка на распространённость предметов: логит → отношение.
|
||||
|
||||
Модель калибрована в апостериорную вероятность, а доля предметов
|
||||
среди кандидатов падает с 42 % вблизи до 3.1 % за 160 м. Поэтому
|
||||
честная вероятность на дальности низка даже у настоящего предмета:
|
||||
замер даёт 0.051 у вставленного человека на 150 м — при 0.003 у
|
||||
обстановки, то есть модель РАЗЛИЧАЕТ (AUC 0.743), но абсолютная
|
||||
величина раздавлена редкостью. Детектору нужно отношение
|
||||
правдоподобий, а не доля, поэтому из логита вычитается типичный
|
||||
для этой полосы — ровно как число лучей уже нормируется на
|
||||
ожидаемое для дальности. Ближе `prior_from` поправка нулевая.
|
||||
"""
|
||||
if self.ref_z is None or prior_from <= 0.0:
|
||||
return np.zeros_like(d)
|
||||
base = float(self._ref_at(np.array([prior_from], np.float32))[0])
|
||||
corr = base - self._ref_at(d)
|
||||
return np.where(d > prior_from, np.maximum(corr, 0.0), 0.0)
|
||||
|
||||
def annotate(self, cands: list[Candidate], prior_from: float = 0.0
|
||||
) -> list[Candidate]:
|
||||
if not cands:
|
||||
return cands
|
||||
X = np.stack([describe_full(c) for c in cands])
|
||||
for c, p in zip(cands, self.score(X)):
|
||||
c.extra["mbon"] = float(p)
|
||||
z = self.logit(X)
|
||||
if prior_from > 0.0:
|
||||
d = np.array([c.d for c in cands], np.float32)
|
||||
z = z + self._prior_corr(d, prior_from)
|
||||
for c, zz in zip(cands, z):
|
||||
c.extra["mbon"] = float(1.0 / (1.0 + np.exp(-zz)))
|
||||
return cands
|
||||
|
||||
# ------------------------------------------------------------------ обучение
|
||||
|
|
@ -199,7 +235,18 @@ class MbonReadout:
|
|||
eps = 1e-7
|
||||
loss = -(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps)).mean()
|
||||
print(f" эпоха {ep + 1:4d}: логистическая потеря {loss:.4f}")
|
||||
self._calibrate(self.bias + (self.w_mbon[a] * v).sum(axis=1), y)
|
||||
zr = self.bias + (self.w_mbon[a] * v).sum(axis=1)
|
||||
self._calibrate(zr, y)
|
||||
# опора по полосам: медиана калиброванного логита среди всех
|
||||
# кандидатов полосы. Первый признак дескриптора — log(d).
|
||||
zc = self.gain * zr + self.shift
|
||||
dd = np.exp(np.asarray(X[:, 0], np.float64))
|
||||
ref = []
|
||||
for lo, hi in zip(self.ref_edges[:-1], self.ref_edges[1:]):
|
||||
m = (dd >= lo) & (dd < hi)
|
||||
ref.append(float(np.median(zc[m])) if m.sum() >= 50
|
||||
else (ref[-1] if ref else 0.0))
|
||||
self.ref_z = np.array(ref, np.float32)
|
||||
|
||||
def _calibrate(self, z: np.ndarray, y: np.ndarray, iters: int = 400) -> None:
|
||||
"""Шкалирование Платта: подобрать наклон и сдвиг по обучающей выборке."""
|
||||
|
|
@ -219,7 +266,9 @@ class MbonReadout:
|
|||
mean=self.mean, scale=self.scale,
|
||||
n_kc=self.cfg.n_kc, claws=self.cfg.claws,
|
||||
sparsity=self.cfg.sparsity, seed=self.cfg.seed,
|
||||
n_pn=self.n_pn)
|
||||
n_pn=self.n_pn, ref_edges=self.ref_edges,
|
||||
ref_z=(self.ref_z if self.ref_z is not None
|
||||
else np.zeros(0, np.float32)))
|
||||
|
||||
@staticmethod
|
||||
def load(path: str | Path) -> "MbonReadout":
|
||||
|
|
@ -233,4 +282,7 @@ class MbonReadout:
|
|||
m.shift = np.float32(d["shift"])
|
||||
m.mean = d["mean"].astype(np.float32)
|
||||
m.scale = d["scale"].astype(np.float32)
|
||||
if "ref_z" in d.files and d["ref_z"].size:
|
||||
m.ref_edges = d["ref_edges"].astype(np.float32)
|
||||
m.ref_z = d["ref_z"].astype(np.float32)
|
||||
return m
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ class Params:
|
|||
enable_mbon: bool = True
|
||||
mbon_power: float = 1.0 # резкость: p**power перед смешиванием
|
||||
mbon_blend: float = 1.0 # 1 — только модель, 0 — только ручная формула
|
||||
# Гашение знакомости с дальностью: за `nov_fade_from` её вклад падает
|
||||
# до нуля к `nov_fade_to`. За 90 м новизна измеримо ВРЕДНА — у
|
||||
# вставленного человека на 120…185 м она 0.150 против 0.199 у
|
||||
# обстановки, AUC 0.293, то есть признак перевёрнут. Гашение подняло
|
||||
# P@100 с 0.31 до 0.48 и P@150 с нуля до 0.32 ценой 3.3 → 5.6 ложных
|
||||
# трека на км на ЗНАКОМОЙ линии и ноля на незнакомой. 0 — не гасить,
|
||||
# это тихий режим для линии с обученной памятью. EXPERIMENTS п. 12.3.
|
||||
nov_fade_from: float = 90.0
|
||||
nov_fade_to: float = 160.0
|
||||
# Поправка оценки модели на распространённость предметов: за этой
|
||||
# дальностью из логита вычитается типичный для полосы. 0 — не
|
||||
# поправлять. Замер — EXPERIMENTS п. 12.3.
|
||||
mbon_prior_from: float = 0.0
|
||||
# Пороги решения (см. descending.py). Вынесены сюда, потому что обученное
|
||||
# считывание меняет цену улики: когда ложные тревоги падают, запас можно
|
||||
# потратить на дальность, опустив порог. Замер — EXPERIMENTS п. 11.
|
||||
|
|
@ -97,6 +110,10 @@ class Params:
|
|||
# (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3.
|
||||
min_hits: int = 2
|
||||
novelty_floor: float = 0.10
|
||||
warn_far: float = 0.5 # порог тревоги на дальнем краю
|
||||
warn_far_from: float = 90.0 # с какой дальности он начинает падать
|
||||
leak_far: float = 0.12 # утечка улики на дальнем краю
|
||||
leak_far_from: float = 90.0 # с какой дальности она начинает падать
|
||||
d_min: float = 4.0
|
||||
d_max: float = 220.0
|
||||
min_rays: int = 4
|
||||
|
|
@ -165,14 +182,20 @@ class FlyGuard:
|
|||
self.emd_bank = EmdBank()
|
||||
self.cx = CentralComplex(use_shape=self.p.use_shape,
|
||||
mbon_power=self.p.mbon_power,
|
||||
mbon_blend=self.p.mbon_blend)
|
||||
mbon_blend=self.p.mbon_blend,
|
||||
leak_far=self.p.leak_far,
|
||||
leak_far_from=self.p.leak_far_from,
|
||||
nov_fade_from=self.p.nov_fade_from,
|
||||
nov_fade_to=self.p.nov_fade_to)
|
||||
# без накопления улик трек подтверждается первым же наблюдением
|
||||
self.dn = (DescendingNeurons(
|
||||
warn_evidence=self.p.warn_evidence,
|
||||
clear_evidence=self.p.clear_evidence,
|
||||
emergency_evidence=self.p.emergency_evidence,
|
||||
min_hits=self.p.min_hits,
|
||||
novelty_floor=self.p.novelty_floor) if self.p.use_tracking
|
||||
novelty_floor=self.p.novelty_floor,
|
||||
warn_far=self.p.warn_far,
|
||||
warn_far_from=self.p.warn_far_from) if self.p.use_tracking
|
||||
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
||||
emergency_evidence=0.0, min_hits=1))
|
||||
self._calib: list[PointCloud2] = []
|
||||
|
|
@ -309,7 +332,7 @@ class FlyGuard:
|
|||
# Строго после накопителя: опора веерного тела входит в признаки
|
||||
# считывания, и на большой дальности это его главный вход.
|
||||
if self.readout is not None:
|
||||
self.readout.annotate(cands)
|
||||
self.readout.annotate(cands, self.p.mbon_prior_from)
|
||||
|
||||
with t("central"):
|
||||
self.cx.update(cands, ego.ds, ego.dt)
|
||||
|
|
|
|||
|
|
@ -307,6 +307,46 @@ def test_mbon_absent_leaves_pipeline_unchanged():
|
|||
assert _quality(c, mbon_blend=1.0) == _quality(c, mbon_blend=0.0)
|
||||
|
||||
|
||||
def test_far_warning_threshold_only_relaxes_beyond_its_distance():
|
||||
"""Послабление далёким трекам не должно трогать ближнюю зону."""
|
||||
from flyguard.descending import DescendingNeurons
|
||||
|
||||
flat = DescendingNeurons()
|
||||
assert flat._warn_at(10.0) == flat._warn_at(199.0) == flat.warn_evidence
|
||||
|
||||
dn = DescendingNeurons(warn_evidence=0.5, warn_far=0.3,
|
||||
warn_far_from=90.0, max_range=200.0)
|
||||
assert dn._warn_at(50.0) == 0.5 # ближняя зона не тронута
|
||||
assert dn._warn_at(90.0) == 0.5 # ровно на границе — тоже
|
||||
assert abs(dn._warn_at(145.0) - 0.4) < 1e-6 # середина
|
||||
assert abs(dn._warn_at(200.0) - 0.3) < 1e-6 # дальний край
|
||||
assert abs(dn._warn_at(400.0) - 0.3) < 1e-6 # и не ниже него
|
||||
|
||||
|
||||
def test_novelty_fades_with_range_and_is_intact_up_close():
|
||||
"""Знакомость должна работать вблизи и не мешать вдали."""
|
||||
from flyguard.central_complex import _quality
|
||||
|
||||
near, far = _cand(), _cand()
|
||||
near.d, far.d = 40.0, 160.0
|
||||
near.novelty = far.novelty = 0.15 # «знакомо», как далёкий предмет
|
||||
|
||||
# вблизи гашения нет: знакомое остаётся придавленным
|
||||
a = _quality(near, nov_fade_from=0.0)
|
||||
b = _quality(near, nov_fade_from=90.0)
|
||||
assert abs(a - b) < 1e-9
|
||||
|
||||
# вдали вклад знакомости снят целиком
|
||||
c = _quality(far, nov_fade_from=0.0)
|
||||
d = _quality(far, nov_fade_from=90.0, nov_fade_to=160.0)
|
||||
assert d > c
|
||||
|
||||
# и в промежутке — частично
|
||||
mid = _cand(); mid.d, mid.novelty = 125.0, 0.15
|
||||
m = _quality(mid, nov_fade_from=90.0, nov_fade_to=160.0)
|
||||
assert _quality(mid, nov_fade_from=0.0) < m < d
|
||||
|
||||
|
||||
# ------------------------------------------------------- модель сенсора: яркость
|
||||
|
||||
def test_injected_intensity_copies_the_surroundings():
|
||||
|
|
|
|||
|
|
@ -113,6 +113,26 @@ def main() -> 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("--min-rays", type=int, default=None,
|
||||
help="сколько лучей минимум образуют кандидата")
|
||||
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"))
|
||||
args = ap.parse_args()
|
||||
|
||||
|
|
@ -148,6 +168,24 @@ def main() -> 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.min_rays is not None:
|
||||
over["min_rays"] = args.min_rays
|
||||
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)
|
||||
readout = None
|
||||
folds = {}
|
||||
|
|
@ -165,12 +203,13 @@ def main() -> None:
|
|||
rows = []
|
||||
for p in find_bags(args.root):
|
||||
t0 = time.time()
|
||||
mem = train_excluding(per_bag, extra, p.name, args.target, args.device)
|
||||
mem = (None if args.no_memory else
|
||||
train_excluding(per_bag, extra, p.name, args.target, args.device))
|
||||
rd = folds.get(p.name, readout) if folds else readout
|
||||
if folds and p.name not in folds and p.name != OBSTACLE_BAG:
|
||||
print(f" внимание: для {p.name} нет своей складки")
|
||||
r = run_bag(p, mem, args.limit, params, readout=rd)
|
||||
r["train_size"] = int(mem.n_seen)
|
||||
r["train_size"] = int(mem.n_seen) if mem is not None else 0
|
||||
rows.append(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} м | "
|
||||
|
|
|
|||
|
|
@ -84,7 +84,17 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
|||
tol = max(3.0, 0.12 * d_true)
|
||||
hit = any(abs(o.distance - d_true) < tol for o in res.decision.objects)
|
||||
fp = sum(1 for o in res.decision.objects if abs(o.distance - d_true) >= tol)
|
||||
records[i].append((d_true, int(hit), fp, lab["hit_rays"]))
|
||||
# Воронка потерь. Лучи в предмет попали — а дальше он может
|
||||
# пропасть на любой из трёх ступеней, и лечатся они по-разному:
|
||||
# нет кандидата — вопрос к кластеризации и разделению фигуры и
|
||||
# фона, нет трека — к сопоставлению по кадрам, нет решения —
|
||||
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
|
||||
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
|
||||
cx = pipes[i].cx
|
||||
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
|
||||
for tr in cx.tracks)
|
||||
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
|
||||
int(cand), int(trk)))
|
||||
|
||||
out = []
|
||||
for (name, lat), rec in zip(scen, records):
|
||||
|
|
@ -94,6 +104,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
|||
out.append(dict(bag=bag.path.name, obj=name, lateral=lat,
|
||||
d=a[:, 0].tolist(), hit=a[:, 1].tolist(),
|
||||
fp=a[:, 2].tolist(), rays=a[:, 3].tolist(),
|
||||
cand=a[:, 4].tolist(), trk=a[:, 5].tolist(),
|
||||
travel=float(travel)))
|
||||
return out
|
||||
|
||||
|
|
@ -121,6 +132,24 @@ def main() -> None:
|
|||
help="порог улики для тревоги (по умолчанию 0.5)")
|
||||
ap.add_argument("--clear", type=float, default=None)
|
||||
ap.add_argument("--min-hits", type=int, default=None)
|
||||
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("--min-rays", type=int, default=None,
|
||||
help="сколько лучей минимум образуют кандидата")
|
||||
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 — не поправлять")
|
||||
args = ap.parse_args()
|
||||
|
||||
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -148,6 +177,24 @@ def main() -> 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.min_rays is not None:
|
||||
over["min_rays"] = args.min_rays
|
||||
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)
|
||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import numpy as np # noqa: E402
|
|||
|
||||
import _bootstrap as B # noqa: F401,E402
|
||||
|
||||
NL = chr(10)
|
||||
|
||||
EDGES = np.array([0, 15, 25, 40, 55, 70, 90, 110, 135, 160, 190, 230], float)
|
||||
MIN_VISIBLE_RAYS = 2 # меньше — предмет физически не освещён лучами
|
||||
|
||||
|
|
@ -59,6 +61,27 @@ def working_range(centres: np.ndarray, p: np.ndarray, thr: float = 0.5) -> float
|
|||
return best
|
||||
|
||||
|
||||
def _funnel(rec: list[dict], obj: str = "человек_стоя") -> None:
|
||||
"""Где теряется предмет: лучи → кандидат → трек → решение."""
|
||||
rows = [r for r in rec if r["obj"] == obj]
|
||||
if not rows:
|
||||
return
|
||||
d = np.concatenate([np.array(r["d"]) for r in rows])
|
||||
rays = np.concatenate([np.array(r["rays"]) for r in rows])
|
||||
cand = np.concatenate([np.array(r["cand"]) for r in rows])
|
||||
trk = np.concatenate([np.array(r["trk"]) for r in rows])
|
||||
hit = np.concatenate([np.array(r["hit"]) for r in rows])
|
||||
vis = rays >= MIN_VISIBLE_RAYS
|
||||
print(f"{NL}воронка потерь для «{obj}», доля от ВИДИМЫХ наблюдений:")
|
||||
print(f"{'полоса':>10}{'видимых':>9}{'кандидат':>10}{'трек':>8}{'решение':>9}{'нет кандидата':>15}{'трек без решения':>18}")
|
||||
for lo, hi in ((6, 30), (30, 50), (50, 70), (70, 90), (90, 120), (120, 160)):
|
||||
m = vis & (d >= lo) & (d < hi)
|
||||
if m.sum() < 30:
|
||||
continue
|
||||
c, k, h = cand[m].mean(), trk[m].mean(), hit[m].mean()
|
||||
print(f"{f'{lo}-{hi} м':>10}{int(m.sum()):9d}{c:10.2f}{k:8.2f}{h:9.2f}{1 - c:15.2f}{max(k - h, 0.0):18.2f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--in", dest="path", default=str(B.ARTIFACTS / "benchmark.json"))
|
||||
|
|
@ -119,6 +142,9 @@ def main() -> None:
|
|||
fig.tight_layout(); fig.savefig(out)
|
||||
print("\nсохранено:", out)
|
||||
|
||||
if all("cand" in r for r in rec):
|
||||
_funnel(rec)
|
||||
|
||||
fp = np.concatenate([np.array(r["fp"]) for r in rec])
|
||||
print(f"посторонних тревог: {fp.sum():.0f} на {fp.size} наблюдений "
|
||||
f"({fp.mean():.3f} на кадр)")
|
||||
|
|
|
|||
Loading…
Reference in a new issue