дальний порог тревоги 0.3 за 90 м: P@150 0.33 -> 0.37; разбор решения по треку

This commit is contained in:
Данил Омелечко 2026-09-23 16:53:00 +03:00
parent 3f8314e908
commit 6685673bdb
6 changed files with 181 additions and 38 deletions

View file

@ -137,10 +137,10 @@ python tools/plot_benchmark.py # кривые и граф
| Метрика | Значение | Чем измерено | | Метрика | Значение | Чем измерено |
|---|---|---| |---|---|---|
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` | | Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
| Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 11.0 % кадров | `tools/evaluate.py --mbon-dir` | | Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 13.3 % кадров | `tools/evaluate.py --mbon-dir` |
| То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без `--mbon-dir` | | То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без `--mbon-dir` |
| На незнакомой линии (памяти нет) | **20.3 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` | | На незнакомой линии (памяти нет) | **21.3 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` |
| Дальность (полигон, 14 004 наблюдения) | рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.33** | `tools/plot_benchmark.py` | | Дальность (полигон, 14 004 наблюдения) | рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.37** | `tools/plot_benchmark.py` |
| Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` | | Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` |
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` | | Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.983** | `tools/train_mbon.py` | | Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.983** | `tools/train_mbon.py` |

File diff suppressed because one or more lines are too long

View file

@ -5,14 +5,14 @@
"path_m": 0.0, "path_m": 0.0,
"alarm_frames": 183, "alarm_frames": 183,
"alarm_rate": 0.9631578947368421, "alarm_rate": 0.9631578947368421,
"fp_objects": 518, "fp_objects": 522,
"fp_tracks": 4, "fp_tracks": 4,
"fp_per_km": NaN, "fp_per_km": NaN,
"fp_median_d": 106.83187139894102, "fp_median_d": 106.83192412328344,
"obj_rate": 0.9947368421052631, "obj_rate": 0.9947368421052631,
"ms_p50": 32.263549999697716, "ms_p50": NaN,
"ms_p95": 33.94519999383192, "ms_p95": NaN,
"train_size": 37073 "train_size": 36428
}, },
{ {
"bag": "doubleT_platform", "bag": "doubleT_platform",
@ -25,9 +25,9 @@
"fp_per_km": 4.981343439320496, "fp_per_km": 4.981343439320496,
"fp_median_d": 33.983066409463774, "fp_median_d": 33.983066409463774,
"obj_rate": null, "obj_rate": null,
"ms_p50": 34.84300000491203, "ms_p50": NaN,
"ms_p95": 38.588240001990926, "ms_p95": NaN,
"train_size": 36151 "train_size": 35491
}, },
{ {
"bag": "roundT_doubleT", "bag": "roundT_doubleT",
@ -40,53 +40,53 @@
"fp_per_km": 4.976117835149752, "fp_per_km": 4.976117835149752,
"fp_median_d": 20.107822042272232, "fp_median_d": 20.107822042272232,
"obj_rate": null, "obj_rate": null,
"ms_p50": 34.14939999493072, "ms_p50": NaN,
"ms_p95": 40.86443000596773, "ms_p95": NaN,
"train_size": 36584 "train_size": 35861
}, },
{ {
"bag": "roundT_pressureGate_roundT", "bag": "roundT_pressureGate_roundT",
"frames": 239, "frames": 239,
"path_m": 246.7637153487153, "path_m": 246.7637153487153,
"alarm_frames": 25, "alarm_frames": 29,
"alarm_rate": 0.10460251046025104, "alarm_rate": 0.12133891213389121,
"fp_objects": 25, "fp_objects": 29,
"fp_tracks": 2, "fp_tracks": 2,
"fp_per_km": 8.104919303770778, "fp_per_km": 8.104919303770778,
"fp_median_d": 160.95257704675265, "fp_median_d": 159.45810767922592,
"obj_rate": null, "obj_rate": null,
"ms_p50": 39.37539999969886, "ms_p50": NaN,
"ms_p95": 41.14257999899564, "ms_p95": NaN,
"train_size": 36718 "train_size": 36114
}, },
{ {
"bag": "roundT_squareT_pressureGate_squareT", "bag": "roundT_squareT_pressureGate_squareT",
"frames": 239, "frames": 239,
"path_m": 273.9439574444854, "path_m": 273.9439574444854,
"alarm_frames": 35, "alarm_frames": 54,
"alarm_rate": 0.14644351464435146, "alarm_rate": 0.22594142259414227,
"fp_objects": 35, "fp_objects": 54,
"fp_tracks": 4, "fp_tracks": 4,
"fp_per_km": 14.601526667404583, "fp_per_km": 14.601526667404583,
"fp_median_d": 105.12193877726114, "fp_median_d": 106.72244931783379,
"obj_rate": null, "obj_rate": null,
"ms_p50": 40.1726000054623, "ms_p50": NaN,
"ms_p95": 42.31233000136854, "ms_p95": NaN,
"train_size": 36083 "train_size": 35654
}, },
{ {
"bag": "squareT_platform_squareT_switch", "bag": "squareT_platform_squareT_switch",
"frames": 239, "frames": 239,
"path_m": 270.98139848673645, "path_m": 270.98139848673645,
"alarm_frames": 40, "alarm_frames": 45,
"alarm_rate": 0.16736401673640167, "alarm_rate": 0.18828451882845187,
"fp_objects": 40, "fp_objects": 45,
"fp_tracks": 2, "fp_tracks": 2,
"fp_per_km": 7.380580405772363, "fp_per_km": 7.380580405772363,
"fp_median_d": 104.76858112973235, "fp_median_d": 107.47124911234653,
"obj_rate": null, "obj_rate": null,
"ms_p50": 37.06099999908474, "ms_p50": NaN,
"ms_p95": 39.86911000647524, "ms_p95": NaN,
"train_size": 33658 "train_size": 33494
} }
] ]

View file

@ -269,6 +269,16 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
и экстренное торможение по надёжному близкому объекту. Пороги с гистерезисом — без него трек и экстренное торможение по надёжному близкому объекту. Пороги с гистерезисом — без него трек
на границе даёт дребезг, а дребезжащая команда торможения хуже её отсутствия. на границе даёт дребезг, а дребезжащая команда торможения хуже её отсутствия.
Порог предупреждения зависит от дальности: до 90 м он 0.5, дальше линейно сходит к 0.3 на
200 м. Далёкая улика физически слабее — четыре-восемь лучей вместо сотни, — а цена ошибки ниже:
до предмета ещё сотня метров и десятки кадров на подтверждение. Экстренный порог не снижается
никогда. Это подняло обнаружение на 150 м с 0.33 до 0.37 без потерь ближе (EXPERIMENTS п. 15.9).
Решение принимается по улике трека, а не по среднему качеству его наблюдений, хотя улика
насыщается: у всего, что видно дольше пары секунд, она равна единице. Проверено три способа
учесть среднее — обученное считывание по треку, жёсткий порог и смешивание, — и ни один не
дал размена лучше простого порога на незнакомой линии (EXPERIMENTS п. 15.5–15.8).
Тормозной путь считается как `v·t_реакции + v²/(2a)` и сравнивается с дистанцией до объекта. Тормозной путь считается как `v·t_реакции + v²/(2a)` и сравнивается с дистанцией до объекта.
--- ---

View file

@ -1398,6 +1398,10 @@ h +0.0055 lat +0.0044
могут пригодиться на линии с другой статистикой. Значения по умолчанию могут пригодиться на линии с другой статистикой. Значения по умолчанию
выключают их полностью. выключают их полностью.
*Позднее:* `warn_far` перемерен на конфигурации после п. 12.3 и 12.5 и
**принят** — P@150 0.33 → 0.37 без потерь ближе. Почему отказ устарел —
п. 15.9.
Итоговая конфигурация и её цена: Итоговая конфигурация и её цена:
| | без гашения | **принято** | | | без гашения | **принято** |
@ -1790,10 +1794,33 @@ P@150 0.32), а на всём, что меньше, — заметно хуже:
потолок. **Тот самый вес, из которого улика складывается, различает их потолок. **Тот самый вес, из которого улика складывается, различает их
идеально.** Сумма потеряла то, что было в слагаемых. идеально.** Сумма потеряла то, что было в слагаемых.
**Где именно это бьёт — оговорка.** `doubleT_obstacle` записан со **стоящего**
поезда, и там наблюдений у каждого трека сотни, так что потолка достигают и
предмет, и обстановка. На полигоне поезд едет, ложный трек столько не живёт, и
картина другая: улика на потолке у 79 % наблюдений предмета в полосе 0…30 м и
лишь у 1.5 % ложных, на 80…110 м — 31 % против 3.4 %.
То есть насыщение само по себе ложных тревог не создаёт. Оно **обесценивает
улику как меру**: у предмета она перестаёт расти, и весь запас, который он
заработал качеством наблюдений, пропадает — вместо «этот трек вчетверо
убедительнее» остаётся «оба по единице». Хуже всего это там, где поезд стоит
или ползёт, то есть ровно в ситуации, когда препятствие опаснее всего.
Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со
средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как
раньше, так что размен меряется, а не объявляется. раньше, так что размен меряется, а не объявляется.
Что смешивание делает с отсчётом, посчитано прямо по выборке. При `b = 0.3`
через порог 0.5 уходят вниз 180 предметных наблюдений и 668 ложных — на одно
потерянное обнаружение три и семь десятых убранных ложных. При `b = 0.2`
соотношение лучше: 73 против 348, то есть 4.8. При `b = 0.5` оно рушится до
1.3, и это то же колено, что видно в таблице п. 15.6.
Смешивание при этом **не только давит**: у 21 % наблюдений отсчёт растёт — это
молодые треки с хорошими наблюдениями, у которых улика ещё мала. Среди ложных
таких даже больше (25.5 % против 14.6 % среди предметных), и на незнакомой
линии, где память ничего не подавляет, это выходит боком (п. 15.8).
### 15.2. Выборка по трекам ### 15.2. Выборка по трекам
Полигон научили попутно выкладывать описание каждого живого трека на каждом Полигон научили попутно выкладывать описание каждого живого трека на каждом
@ -1941,3 +1968,100 @@ leave-one-bag-out.
(п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но (п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но
смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой, смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой,
поэтому порог оставлен выключенным (`track_gate = 0`). поэтому порог оставлен выключенным (`track_gate = 0`).
### 15.8. Смешивание меняет не в ту сторону
Ложные треки на километр, обе сцены, считывание по складкам:
| настройка | знакомая линия | незнакомая линия | объект на 55 м |
|---|---:|---:|---:|
| база | 8.0 | **20.3** | 99.5 % |
| один дальний порог 0.30 | **8.0** | 21.3 | 99.5 % |
| смешивание 0.3 + дальний порог с 60 м | 6.3 | **24.8** | 99.5 % |
| смешивание 0.2 + дальний порог с 60 м | 8.0 | 24.0 | 99.5 % |
Смешивание улучшает знакомую линию и **портит незнакомую**. Это не случайность,
и механизм виден из разбора п. 15.1: смешивание не только давит — у 21 %
наблюдений оно отсчёт поднимает, и среди ложных таких больше, чем среди
предметных (25.5 % против 14.6 %). Поднимаются молодые треки с хорошими
наблюдениями и малой уликой. На знакомой линии память тоннеля такие треки уже
погасила через новизну, а на незнакомой гасить нечем — и смешивание их
вытаскивает.
Приватный тест — это незнакомая линия. Значит, смешивание отвергается:
`track_blend = 0` по умолчанию.
И это ещё не всё. Смешивание надо сравнивать не с базой, а с **тем же разменом,
полученным простым поднятием порога** — иначе мы сравниваем две разные рабочие
точки и выдаём сдвиг по кривой за улучшение кривой. Контроль:
| настройка | P@50 | P@100 | P@150 | посторонних |
|---|---|---|---|---:|
| смешивание 0.2 + дальний порог с 60 м | 0.71 | 0.47 | 0.37 | 817 |
| **порог 0.60 + дальний порог, без смешивания** | 0.70 | **0.49** | 0.37 | **816** |
Одинаковое число посторонних тревог, и при нём простой порог даёт обнаружение
на 100 м **лучше**. Никакой новой кривой смешивание не открывает — оно двигает
по той же, что и ключ, который у нас уже был.
Итог: `track_blend = 0`, `track_gate = 0`. Накопители на треке и считывание по
трекам остаются в коде как инструмент замера — они и дали понимание, что улика
насыщается, — но в решении не участвуют.
### 15.9. Отрицательные результаты стареют
Выигрыш в этой главе дал не считывание по трекам, ради которого всё
затевалось, а ключ, **отвергнутый тремя разделами раньше**. П. 12.2 говорит
дословно: «порог тревоги, линейно опущенный с 0.5 до 0.3 начиная с 90 м, не
изменил ни ложных (3.3 на км), ни обнаружения (P@100 = 0.31, решения за 120 м
по-прежнему 0)».
Тогда это было правдой, и причина там же, в п. 12.3: улика далёкого трека
колебалась около нуля, потому что знакомость была на той дальности перевёрнута
и входила в вес **множителем**. Опускать порог под сигналом, который равен
нулю, бессмысленно — что замер и показал.
Потом приняли гашение знакомости с 90 м (п. 12.3) и порог по лучам,
нормированный на дальность (п. 12.5). Улика далёкого трека перестала быть
нулём. Тот же самый ключ, тот же самый замер — и P@150 растёт с 0.33 до 0.37
ценой нуля ложных треков на знакомой линии и одного на незнакомой.
Отсюда правило, которое стоит дороже самой правки: **отрицательный результат
верен только для той конфигурации, в которой он получен.** Каждое принятое
изменение обесценивает часть прежних отказов, и дешёвые из них надо
перепроверять. Здесь это стоило одного прогона на четыре минуты.
Варианты той же правки, полигон, `человек_стоя`:
| настройка | P@50 | P@100 | P@150 | посторонних |
|---|---|---|---|---:|
| база | 0.70 | 0.53 | 0.33 | 999 |
| **порог 0.30 с 90 м** | 0.70 | 0.53 | **0.37** | **1091** |
| порог 0.30 с 60 м | 0.70 | 0.53 | 0.37 | 1197 |
| порог 0.20 с 90 м | 0.70 | 0.53 | 0.38 | 1181 |
| порог 0.30 + утечка 0.03 | 0.70 | **0.48** | 0.40 | 1172 |
Начинать раньше незачем: обнаружение то же, ложных больше. Опускать до 0.20 —
сотая на 150 м за восемь процентов посторонних тревог. Утечку, отвергнутую
в п. 12.4, на нынешней базе тоже перемерили: теперь она даёт P@150 0.40, но
забирает 100 м (0.53 → 0.48) и рабочую дальность 100 → 80 м — отвергнута
снова, уже по другой причине. Правило «перепроверять» не означает
«перепринимать».
Принято: `warn_far = 0.30`, `warn_far_from = 90`. Итог на всех сценах:
| | было | **стало** |
|---|---:|---:|
| P@150, человек стоя | 0.33 | **0.37** |
| P@150, человек сидя | 0.22 | **0.24** |
| P@100 / P@150, ящик | 0.38 / 0.03 | **0.40 / 0.06** |
| P@50 и P@100, человек | 0.70 / 0.53 | 0.70 / 0.53 |
| решений в полосе 90–120 м / 120–160 м | 0.49 / 0.38 | **0.51 / 0.41** |
| ложных треков на км, знакомая линия | 8.0 | **8.0** |
| то же, незнакомая линия | 20.3 | 21.3 |
| настоящий объект на 55 м | 99.5 % | **99.5 %** |
Экстренного торможения правка не касается: у него свой порог и условие
`d ≤ max(тормозной путь, 25 м)`, поэтому далёкий ложный трек физически не
может его вызвать. Мы снова платим предупреждениями за дальность — для
движущегося поезда это правильная сторона размена (как и в п. 12.3).

View file

@ -125,8 +125,17 @@ class Params:
# (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3. # (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3.
min_hits: int = 2 min_hits: int = 2
novelty_floor: float = 0.10 novelty_floor: float = 0.10
warn_far: float = 0.5 # порог тревоги на дальнем краю # Порог тревоги, линейно опускаемый с 0.5 до `warn_far` на отрезке от
warn_far_from: float = 90.0 # с какой дальности он начинает падать # `warn_far_from` до 200 м. В п. 12.2 ровно эта правка была отвергнута:
# тогда улика далёкого трека колебалась около нуля, и опускать порог под
# нулевым сигналом было бессмысленно. После гашения знакомости и порога
# по лучам улика перестала быть нулём, и та же правка дала P@150
# 0.33 → 0.37 без потерь на 50 и 100 м ценой нуля ложных треков на
# знакомой линии и одного на незнакомой. Экстренного торможения она не
# касается: у него свой порог и условие d ≤ max(тормозной путь, 25 м).
# EXPERIMENTS п. 15.9.
warn_far: float = 0.30
warn_far_from: float = 90.0
leak_far: float = 0.12 # утечка улики на дальнем краю leak_far: float = 0.12 # утечка улики на дальнем краю
leak_far_from: float = 90.0 # с какой дальности она начинает падать leak_far_from: float = 90.0 # с какой дальности она начинает падать
d_min: float = 4.0 d_min: float = 4.0