резкость считывания 1.5: ложных на новой линии 23.5 -> 14.7 на км, память пересобрана на новом полу, new_data читается кусками из tar
This commit is contained in:
parent
6964e4cc4e
commit
28fd716a82
11 changed files with 474 additions and 55 deletions
24
README.md
24
README.md
|
|
@ -143,6 +143,19 @@ python tools/plot_benchmark.py # кривые и граф
|
|||
python tools/compare_benchmark.py было.json стало.json # правка парно
|
||||
```
|
||||
|
||||
Большой бэг `new_data` (90 ГБ) не распаковывается целиком — инструменты
|
||||
читают его кусками прямо из архива (`--tar`, по умолчанию
|
||||
`../датасет/new_data` или `FLYGUARD_NEW_DATA`):
|
||||
|
||||
```bash
|
||||
python tools/make_training_set.py --new-data 0:110 --out data/cache/training_set_nd.npz
|
||||
python tools/train_mbon.py --device cuda --data data/cache/training_set.npz --data data/cache/training_set_nd.npz --train-only new_data_ --out nd.npz
|
||||
python tools/eval_new_data.py --shards 110: --readout было=artifacts/mbon_readout.npz --readout стало=nd.npz --out nd.json
|
||||
```
|
||||
|
||||
Проверка на второй половине честная: вставки идут только в первую, а
|
||||
вторую модель не видит ни в каком виде (EXPERIMENTS п. 17.3).
|
||||
|
||||
Любое поле `Params` меняется без правки кода: `--set h_lo_core=0.28 --set
|
||||
k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py` и
|
||||
`check_obstacle.py`. Два прогона полигона сравнивайте только парно —
|
||||
|
|
@ -168,11 +181,12 @@ k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py` и
|
|||
| Метрика | Значение | Чем измерено |
|
||||
|---|---|---|
|
||||
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
||||
| Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 13.8 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||
| То же без обученного считывания | 13.6 на км, 28.1 % кадров | там же, без `--mbon-dir` |
|
||||
| На незнакомой линии (памяти нет) | **23.5 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
||||
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность 80 м, P@50 = 0.71, P@100 = 0.48, **P@150 = 0.49** | `tools/plot_benchmark.py` |
|
||||
| Человек, упавший на пути | P@50 = **0.50** (было 0.27), рабочая дальность 32 м | там же |
|
||||
| Ложные тревоги, leave-one-bag-out | **5.7 трека на км**, 8.8 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||
| То же без обученного считывания | 11.9 на км, 20.3 % кадров | там же, без `--mbon-dir` |
|
||||
| На незнакомой линии (памяти нет) | **14.7 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
||||
| Вторая половина `new_data`: другой день, 3.27 км, не видена при обучении | **17.4 на км**, 31.4 % кадров | `tools/eval_new_data.py` |
|
||||
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность **100 м**, P@50 = 0.71, P@100 = 0.50, P@150 = 0.46 | `tools/plot_benchmark.py` |
|
||||
| Человек, упавший на пути | P@50 = **0.50** (было 0.27), рабочая дальность 20 м | там же |
|
||||
| Обработка кадра | медиана 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
|
|
@ -3,31 +3,31 @@
|
|||
"bag": "doubleT_obstacle",
|
||||
"frames": 190,
|
||||
"path_m": 0.0,
|
||||
"alarm_frames": 183,
|
||||
"alarm_rate": 0.9631578947368421,
|
||||
"fp_objects": 522,
|
||||
"fp_tracks": 4,
|
||||
"alarm_frames": 153,
|
||||
"alarm_rate": 0.8052631578947368,
|
||||
"fp_objects": 336,
|
||||
"fp_tracks": 5,
|
||||
"fp_per_km": NaN,
|
||||
"fp_median_d": 106.83192412328344,
|
||||
"fp_median_d": 114.4002904954952,
|
||||
"obj_rate": 0.9947368421052631,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 36428
|
||||
"train_size": 48221
|
||||
},
|
||||
{
|
||||
"bag": "doubleT_platform",
|
||||
"frames": 239,
|
||||
"path_m": 200.74905739412534,
|
||||
"alarm_frames": 48,
|
||||
"alarm_rate": 0.200836820083682,
|
||||
"fp_objects": 66,
|
||||
"alarm_frames": 31,
|
||||
"alarm_rate": 0.1297071129707113,
|
||||
"fp_objects": 42,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 9.962686878640993,
|
||||
"fp_median_d": 32.8633359224923,
|
||||
"fp_median_d": 34.00916383990402,
|
||||
"obj_rate": null,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 35491
|
||||
"train_size": 46850
|
||||
},
|
||||
{
|
||||
"bag": "roundT_doubleT",
|
||||
|
|
@ -42,51 +42,51 @@
|
|||
"obj_rate": null,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 35861
|
||||
"train_size": 47438
|
||||
},
|
||||
{
|
||||
"bag": "roundT_pressureGate_roundT",
|
||||
"frames": 239,
|
||||
"path_m": 246.7637153487153,
|
||||
"alarm_frames": 25,
|
||||
"alarm_rate": 0.10460251046025104,
|
||||
"fp_objects": 25,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 8.104919303770778,
|
||||
"fp_median_d": 160.95291512943237,
|
||||
"alarm_frames": 18,
|
||||
"alarm_rate": 0.07531380753138076,
|
||||
"fp_objects": 18,
|
||||
"fp_tracks": 1,
|
||||
"fp_per_km": 4.052459651885389,
|
||||
"fp_median_d": 160.20564869629789,
|
||||
"obj_rate": null,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 36114
|
||||
"train_size": 47718
|
||||
},
|
||||
{
|
||||
"bag": "roundT_squareT_pressureGate_squareT",
|
||||
"frames": 239,
|
||||
"path_m": 273.9439574444854,
|
||||
"alarm_frames": 6,
|
||||
"alarm_rate": 0.02510460251046025,
|
||||
"fp_objects": 6,
|
||||
"fp_tracks": 2,
|
||||
"fp_per_km": 7.3007633337022915,
|
||||
"fp_median_d": 161.27067444604893,
|
||||
"alarm_frames": 4,
|
||||
"alarm_rate": 0.016736401673640166,
|
||||
"fp_objects": 4,
|
||||
"fp_tracks": 1,
|
||||
"fp_per_km": 3.6503816668511457,
|
||||
"fp_median_d": 162.18072351126122,
|
||||
"obj_rate": null,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 35654
|
||||
"train_size": 46546
|
||||
},
|
||||
{
|
||||
"bag": "squareT_platform_squareT_switch",
|
||||
"frames": 239,
|
||||
"path_m": 270.98139848673645,
|
||||
"alarm_frames": 86,
|
||||
"alarm_rate": 0.3598326359832636,
|
||||
"fp_objects": 91,
|
||||
"fp_tracks": 4,
|
||||
"fp_per_km": 14.761160811544727,
|
||||
"fp_median_d": 96.62033146128638,
|
||||
"alarm_frames": 52,
|
||||
"alarm_rate": 0.2175732217573222,
|
||||
"fp_objects": 56,
|
||||
"fp_tracks": 3,
|
||||
"fp_per_km": 11.070870608658545,
|
||||
"fp_median_d": 90.95581811929335,
|
||||
"obj_rate": null,
|
||||
"ms_p50": NaN,
|
||||
"ms_p95": NaN,
|
||||
"train_size": 33494
|
||||
"train_size": 44039
|
||||
}
|
||||
]
|
||||
Binary file not shown.
|
|
@ -299,6 +299,7 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
|
|||
| `fov_deg` | шире сектор → лучше кривые и стрелки, дороже обработка |
|
||||
| `gain` / `leak` (CX) | быстрее подтверждение против устойчивости к шуму |
|
||||
| `warn_evidence` | выше → меньше ложных тревог, позже обнаружение |
|
||||
| `mbon_power` | вес наблюдения — вероятность обученного считывания в этой степени. Выше → строже: 1.0 → 1.5 срезает ложные на незнакомой линии с 23.5 до 14.7 на км ценой хвоста дальности за 130 м; 2.0 почти ничего не добавляет к ложным и теряет вдвое больше (EXPERIMENTS п. 17.4) |
|
||||
| темп депрессии MB | выше → сильнее подавление знакомого, но риск заглушить и настоящее |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -2420,4 +2420,181 @@ leave-one-bag-out.
|
|||
собранных 19.09 при прежнем полу, и низких конструкций в колее не знает: на
|
||||
знакомой линии их гасит только считывание. Выборку памяти надо собирать
|
||||
проходом по 90 ГБ `new_data` — это следующий шаг, если понадобится ещё
|
||||
снизить ложные на знакомой линии.
|
||||
снизить ложные на знакомой линии. *Сделано — п. 17.1.*
|
||||
|
||||
## 17. После переноса: память на новом полу, край габарита, вторая половина `new_data`
|
||||
|
||||
Три хвоста п. 16, каждый мерился так же: полигон парно, ложные треки на двух
|
||||
сценах, настоящий объект.
|
||||
|
||||
### 17.1. Память тоннеля на новом полу
|
||||
|
||||
Кэш кандидатов памяти был собран 19.09, при старом полу, и низких конструкций
|
||||
между рельсами память не знала (п. 16.8). Пересобран целиком: шесть записей
|
||||
(`tune_memory.py --collect-only`) и весь `new_data` (`stream_new_data.py`,
|
||||
39 707 кандидатов против 30 902), память переобучена.
|
||||
|
||||
| | было | новая память |
|
||||
|---|---:|---:|
|
||||
| ложных треков/км, знакомая линия | 8.0 | **6.5** |
|
||||
| кадров с тревогой | 13.8 % | **9.0 %** |
|
||||
| посторонних на полигоне | 1226 | 1166 |
|
||||
| полигон парно | | +78 / −95 |
|
||||
| настоящий объект | 99.5 % | 99.5 % |
|
||||
|
||||
Незнакомой линии память не касается — там её нет по построению.
|
||||
|
||||
При резкости считывания 1.5, принятой в п. 17.4, разница между памятями
|
||||
исчезает: на знакомой линии 5.7 и 5.7 ложных трека на км, полигон парно
|
||||
+90 / −86, на записи с настоящим объектом кадров с посторонней тревогой
|
||||
80.5 % против 96.3 %. Принята новая: она собрана на том же полу габарита,
|
||||
что и детектор, и знает низкие конструкции между рельсами, которые детектор
|
||||
теперь видит.
|
||||
|
||||
Все ближние потери — один сценарий: на `roundT_pressureGate_roundT` поезд
|
||||
стоит в 14.1 м от сидящего человека или ящика у края габарита, рядом
|
||||
гермозатвор, и предмет не подтверждается: 26 кадров подряд у сидящего, 10 у
|
||||
ящика. Кандидат там —
|
||||
слипшееся пятно (≈950 лучей, высота 0.40 м), считывание в нём не уверено
|
||||
(p = 0.23–0.30), и держится он только на новизне: у старой памяти 0.27–0.28,
|
||||
у новой 0.20–0.21. При нижнем пороге новизны 0.15 вес наблюдения падает в
|
||||
2.4 раза, улика 0.36–0.39 против 0.04–0.05. Случай пограничный при любой
|
||||
памяти, новая переводит его через край. У настоящего объекта новизна тоже
|
||||
ниже (медиана 0.63 → 0.51), но выше 0.50, где множитель новизны уже равен
|
||||
единице.
|
||||
|
||||
### 17.2. Край габарита у платформы
|
||||
|
||||
Шестнадцать ближних потерь после переобучения (п. 16.3) разобраны. У края
|
||||
габарита (0.9 м) рядом с платформой нижняя часть человека слипается с
|
||||
кромкой, и от кадра к кадру кандидат то слипшееся пятно (p ≈ 0.0–0.2 у любых
|
||||
складок), то «верх»: обрывок 0.2 × 0.2 м на высоте 1.5–1.7 м, целиком в
|
||||
габарите, с нулевым дефицитом до пола — по признакам навесное оборудование.
|
||||
Такой верх старые складки оценивали в 0.74–0.99, новые — в 0.43–0.84. Улика
|
||||
не добирает до порога 0.5, и решение приходит на 8.8–8.9 м вместо 10.7–17 м.
|
||||
|
||||
В среднем новые складки на ближних предметах почти так же уверены, как
|
||||
старые (отложенные записи, 0–15 м: медиана p 0.993 против 0.996, доля
|
||||
p ≥ 0.9 — 0.864 против 0.891), а уверенного фона на 60–120 м у них вшестеро
|
||||
меньше (0.1 % против 0.7 %). Отсюда и падение ложных тревог: новое считывание
|
||||
строже в целом и к нетипичному обрывку в частности.
|
||||
|
||||
Попытка лечения — положение 0.9 м в обучающей выборке: полигон его проверяет,
|
||||
а в выборке были только 0, ±0.6 и ±1.2 м. Выборка 466 715 кандидатов, AUC
|
||||
0.9849. **Край это не вылечило**: человек стоя на 0–15 м +5 / −5, сидя
|
||||
+0 / −4. Зато считывание стало строже к фону:
|
||||
|
||||
| | было | с ±0.9 |
|
||||
|---|---:|---:|
|
||||
| ложных треков/км, незнакомая линия | 23.5 | **21.9** |
|
||||
| то же, знакомая (новая память) | 6.5 | 6.5 |
|
||||
| посторонних на полигоне | 1226 | 1175 |
|
||||
| полигон парно | | +9 / −25 |
|
||||
| настоящий объект | 99.5 % | 99.5 % |
|
||||
|
||||
На второй половине `new_data` (п. 17.3) — ничего: 23.9 и 23.9 ложных трека
|
||||
на км. Не принято.
|
||||
|
||||
### 17.3. Считывание, обученное на `new_data`
|
||||
|
||||
Пять коротких записей сняты в один день (02.09) и вместе дают чуть больше
|
||||
километра пути. `new_data` — двадцать минут непрерывной езды 17.09: 11 271
|
||||
кадр в 221 шарде, 90 ГБ несжатого tar. Места на диске меньше архива, поэтому
|
||||
он читается кусками прямо из tar: `tools/_new_data.py` распаковывает по пять
|
||||
шардов во временный каталог и удаляет за собой.
|
||||
|
||||
Делить пришлось по времени, а не по кадрам: вставки в первую половину
|
||||
(шарды 0–109) идут в обучение, ложные тревоги меряются на второй (шарды
|
||||
110–220, 3.27 км пути), которую модель не видела ни в каком виде. Памяти
|
||||
тоннеля в этой проверке нет, как на незнакомой линии. Выборка — прежние
|
||||
333 739 кандидатов плюс 827 314 из 22 кусков первой половины, предметов
|
||||
5.4 %, AUC складок 0.981.
|
||||
|
||||
| считывание | вторая половина `new_data`, ложных/км | кадров с тревогой | незнакомая линия | знакомая линия |
|
||||
|---|---:|---:|---:|---:|
|
||||
| прежнее | 23.9 | 38.5 % | 23.5 | 6.5 |
|
||||
| с ±0.9 (п. 17.2) | 23.9 | 39.0 % | 21.9 | 6.5 |
|
||||
| с `new_data` | **13.2** | **20.2 %** | **13.2** | 6.2 |
|
||||
|
||||
Ложных тревог почти вдвое меньше, и прокси незнакомой линии совпал с
|
||||
честной проверкой. Но полигон парно — +39 / −259: у стоящего человека P@50
|
||||
0.71 → 0.62, у лежачего P@100 0.22 → 0.11, у ящика 0.53 → 0.40.
|
||||
|
||||
Воспроизводится так:
|
||||
|
||||
```bash
|
||||
python tools/make_training_set.py --new-data 0:110 --out data/cache/training_set_nd.npz
|
||||
python tools/train_mbon.py --device cuda --data data/cache/training_set.npz --data data/cache/training_set_nd.npz --train-only new_data_ --save-folds artifacts/mbon_folds_nd --out artifacts/mbon_readout_nd.npz
|
||||
python tools/eval_new_data.py --shards 110: --readout было=artifacts/mbon_readout.npz --readout nd=artifacts/mbon_readout_nd.npz --out nd_eval.json
|
||||
```
|
||||
|
||||
`--train-only new_data_` — куски `new_data` только учат и складок не
|
||||
получают: проверяются по-прежнему пять записей.
|
||||
|
||||
### 17.4. Строже или умнее: сравнение при равной строгости
|
||||
|
||||
Модель, которая вдвое реже тревожится и заметно реже видит предмет, может
|
||||
быть не лучше, а просто строже. Это проверяется сравнением при одинаковой
|
||||
строгости, и ручка для неё есть с п. 11: `mbon_power`, вес наблюдения
|
||||
p^power. Все строки — с новой памятью (п. 17.1).
|
||||
|
||||
| считывание, резкость | вторая половина `new_data` | незнакомая | знакомая | посторонних на полигоне | полигон парно к первой строке |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| прежнее, 1.0 | 23.9 | 23.5 | 6.5 | 1166 | |
|
||||
| **прежнее, 1.5** | 17.4 | 14.7 | **5.7** | **793** | **+1 / −142** |
|
||||
| прежнее, 2.0 | 15.0 | 14.3 | 5.7 | 628 | +0 / −268 |
|
||||
| `new_data`, 1.0 | **13.2** | **13.2** | 6.2 | 822 | +39 / −259 |
|
||||
| `new_data`, 0.7 | 24.8 | 16.4 | 7.7 | 927 | +72 / −184 |
|
||||
|
||||
* **Больше половины выигрыша — строгость.** Прежнее считывание при
|
||||
резкости 1.5 срезает ложные на незнакомой линии с 23.5 до 14.7, а на
|
||||
полигоне теряет 142 наблюдения из 7 600: 39 вблизи у края габарита (тот же
|
||||
пограничный случай, что в п. 17.1–17.2) и 82 за 90 м.
|
||||
* **Но не весь.** При сопоставимых ложных на второй половине `new_data`
|
||||
(15.0 и 13.2) модель на `new_data` теряет на полигоне меньше, чем прежняя
|
||||
при 2.0 (−220 против −268 в сумме). Другой день записи действительно
|
||||
научил её тому, чего в пяти записях нет. Ослаблять её бесполезно: при 0.7
|
||||
ложные возвращаются к 24.8.
|
||||
* **Между «прежнее, 1.5» и «`new_data`, 1.0» — размен, а не доминирование.**
|
||||
Вторая на четверть реже тревожится на данных своего дня, первая держит
|
||||
дальность: P@50 у стоящего 0.71 против 0.62, P@100 у лежачего 0.22 против
|
||||
0.11, у ящика 0.50 против 0.40. На пяти записях без памяти, которых при
|
||||
проверке не видела ни одна из двух, они почти равны (14.7 и 13.2), на
|
||||
знакомой линии прежняя лучше (5.7 и 6.2).
|
||||
|
||||
**Принято: прежнее считывание, резкость 1.5.** ТЗ просит баланс дальности и
|
||||
ложных тревог, постановщик — прежде всего не видеть того, чего нет; резкость
|
||||
1.5 даёт второе почти без цены в первом. Модель на `new_data` остаётся
|
||||
рецептом из п. 17.3: если появятся ещё записи других дней, учить на них
|
||||
выгоднее, чем ужесточать.
|
||||
|
||||
### 17.5. Итог
|
||||
|
||||
Новая память (п. 17.1), прежнее считывание, резкость 1.5 (п. 17.4) — против
|
||||
опубликованного 23.09:
|
||||
|
||||
| | 23.09 | сейчас |
|
||||
|---|---:|---:|
|
||||
| ложных треков/км, знакомая линия | 8.0 (13.8 % кадров) | **5.7** (8.8 %) |
|
||||
| то же, незнакомая линия | 23.5 (34.6 %) | **14.7** (25.9 %) |
|
||||
| то же, вторая половина `new_data` | 23.9 (38.5 %) | **17.4** (31.4 %) |
|
||||
| посторонних тревог на полигоне | 1226 | **793** |
|
||||
| настоящий объект | 99.5 % | 99.5 % |
|
||||
| человек стоя: рабочая дальность; P@50 / P@100 / P@150 | 80 м; 0.71 / 0.48 / 0.49 | **100 м**; 0.71 / 0.50 / 0.46 |
|
||||
| человек сидя | 148 м; 0.67 / 0.52 / 0.49 | 122 м; 0.67 / 0.54 / 0.39 |
|
||||
| человек лёжа | 32 м; 0.50 / 0.22 / 0.02 | 20 м; 0.50 / 0.22 / 0.00 |
|
||||
| ящик | 20 м; 0.54 / 0.50 / 0.26 | 20 м; 0.57 / 0.50 / 0.22 |
|
||||
| чемодан | 80 м; 0.61 / 0.45 / 0.24 | 80 м; 0.61 / 0.46 / 0.24 |
|
||||
|
||||
Ложных тревог примерно на треть меньше на всех трёх сценах, ближняя и
|
||||
средняя дальность на месте. Цена — хвост за 130 м у сидящего человека
|
||||
(P@150 0.49 → 0.39) и рабочая дальность лежачего 32 → 20 м: доля в полосе
|
||||
20–32 м у него ходит у самого порога 0.5, и сдвинула её новая память, а не
|
||||
резкость (P@50 тот же, 0.50). Без обученного считывания при тех же
|
||||
настройках было бы 11.9 и 36.1 ложных трека на км.
|
||||
|
||||
Отдельно найдено при проверке сдачи: образ Docker **не загружал**
|
||||
считывание — `mbon_path` был пустым, файла модели в образе не было, и узел
|
||||
молча работал на ручной формуле. Все цифры выше замерены со считыванием,
|
||||
образ теперь тоже с ним (`docker/Dockerfile`, `detect.launch.py`), а
|
||||
дымовой тест образа проверяет, что модель на месте.
|
||||
|
|
|
|||
|
|
@ -103,7 +103,13 @@ class Params:
|
|||
# веса улики в центральном комплексе там, где модель передана конвейеру;
|
||||
# без модели всё работает по-прежнему.
|
||||
enable_mbon: bool = True
|
||||
mbon_power: float = 1.0 # резкость: p**power перед смешиванием
|
||||
# Резкость: вес наблюдения p**power. Рабочая точка 1.5 выбрана сравнением
|
||||
# при равной строгости (EXPERIMENTS п. 17.4): ложных треков на незнакомой
|
||||
# линии 23.5 → 14.7 на км, на знакомой 6.5 → 5.7, на второй половине
|
||||
# new_data 23.9 → 17.4, а полигон теряет 142 наблюдения из 7 600: 39
|
||||
# вблизи у края габарита (пограничный случай п. 17.1–17.2) и 82 за 90 м.
|
||||
# 2.0 срезает ещё 127 наблюдений ради 0.4 трека на км.
|
||||
mbon_power: float = 1.5
|
||||
mbon_blend: float = 1.0 # 1 — только модель, 0 — только ручная формула
|
||||
# Гашение знакомости с дальностью: за `nov_fade_from` её вклад падает
|
||||
# до нуля к `nov_fade_to`. За 90 м новизна измеримо ВРЕДНА — у
|
||||
|
|
|
|||
72
tools/_new_data.py
Normal file
72
tools/_new_data.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Большой бэг `new_data` кусками прямо из tar-архива.
|
||||
|
||||
Это 90 ГБ в 221 шарде, а свободного места на диске меньше, чем весь архив.
|
||||
Кусок из нескольких подряд идущих шардов распаковывается во временный
|
||||
каталог, читается как обычный многошардовый бэг (`flyguard.bag.Bag`) и
|
||||
удаляется. Шард достаётся по смещению в архиве, без повторного разбора
|
||||
заголовков, поэтому куски можно распаковывать из нескольких процессов сразу.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
# Архив лежит там, куда его положили при скачивании датасета, а не в data/:
|
||||
# распаковывать его целиком некуда. Путь переопределяется FLYGUARD_NEW_DATA
|
||||
# или ключом --tar у инструментов.
|
||||
DEFAULT_TAR = os.environ.get(
|
||||
"FLYGUARD_NEW_DATA",
|
||||
str(Path(__file__).resolve().parents[1] / "датасет" / "new_data"))
|
||||
SHARD_RE = re.compile(r"_(\d+)\.db3$")
|
||||
|
||||
|
||||
def shards(tar_path: str | Path) -> list[tuple[int, str, int, int]]:
|
||||
"""(номер, имя файла, смещение данных, размер) всех шардов, по номеру."""
|
||||
out = []
|
||||
with tarfile.open(tar_path, "r:") as t:
|
||||
for m in t:
|
||||
mm = SHARD_RE.search(m.name)
|
||||
if mm and m.isfile():
|
||||
out.append((int(mm.group(1)), Path(m.name).name,
|
||||
m.offset_data, m.size))
|
||||
out.sort()
|
||||
return out
|
||||
|
||||
|
||||
def pick(members: list, spec: str) -> list:
|
||||
"""Шарды по срезу номеров: `110:` — со 110-го до конца, `0:110` — первые 110."""
|
||||
lo, _, hi = spec.partition(":")
|
||||
lo_i = int(lo) if lo else 0
|
||||
hi_i = int(hi) if hi else None
|
||||
return [m for m in members if m[0] >= lo_i and (hi_i is None or m[0] < hi_i)]
|
||||
|
||||
|
||||
def split(members: list, per_chunk: int) -> list[list]:
|
||||
"""Подряд идущие шарды группами: каждая группа — одна «запись»."""
|
||||
return [members[i:i + per_chunk] for i in range(0, len(members), per_chunk)]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def chunk(tar_path: str | Path, members: list, workdir: str | None = None):
|
||||
"""Распаковать шарды во временный каталог, отдать его путь, потом удалить."""
|
||||
d = Path(tempfile.mkdtemp(prefix="fg_nd_", dir=workdir))
|
||||
try:
|
||||
with open(tar_path, "rb") as src:
|
||||
for _, name, off, size in members:
|
||||
src.seek(off)
|
||||
with open(d / name, "wb") as dst:
|
||||
left = size
|
||||
while left:
|
||||
buf = src.read(min(left, 1 << 22))
|
||||
if not buf:
|
||||
raise OSError(f"архив обрезан на {name}")
|
||||
dst.write(buf)
|
||||
left -= len(buf)
|
||||
yield d
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
104
tools/eval_new_data.py
Normal file
104
tools/eval_new_data.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Ложные тревоги на участке `new_data`, которого считывание не видело.
|
||||
|
||||
Прокси незнакомой линии в `evaluate.py --no-memory` — пять коротких записей,
|
||||
вместе чуть больше километра пути, и все они сняты в один день. `new_data` —
|
||||
двадцать минут непрерывной езды другим днём. Если учить считывание на
|
||||
вставках в первую половину, а ложные тревоги мерить на второй, проверка
|
||||
остаётся честной при любом ответе на вопрос, та ли это линия: второй половины
|
||||
модель не видела ни в каком виде.
|
||||
|
||||
Вторая половина идёт одним непрерывным потоком — состояние конвейера (поза,
|
||||
ось, треки) переносится между кусками, как в `stream_new_data.py`. Памяти
|
||||
тоннеля нет. Несколько вариантов считывания гоняются по одним и тем же
|
||||
кадрам, поэтому разница между ними — только от считывания. Вариант может
|
||||
нести и свои настройки конвейера через `@`: так сравниваются модели при
|
||||
одинаковой строгости (`@mbon_power=2`), а не только при умолчаниях.
|
||||
|
||||
python tools/eval_new_data.py --shards 110: \\
|
||||
--readout было=artifacts/mbon_readout.npz --readout стало=new.npz \\
|
||||
--readout строже=artifacts/mbon_readout.npz@mbon_power=2 \\
|
||||
--out nd_eval.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
|
||||
import _bootstrap as B # noqa: F401
|
||||
import _new_data as ND
|
||||
import _params as PS
|
||||
from flyguard.bag import Bag
|
||||
from flyguard.mbon_readout import MbonReadout
|
||||
from flyguard.pipeline import FlyGuard, Params
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--tar", default=ND.DEFAULT_TAR)
|
||||
ap.add_argument("--shards", default="110:",
|
||||
help="номера шардов срезом; по умолчанию вторая половина")
|
||||
ap.add_argument("--per-chunk", type=int, default=5,
|
||||
help="сколько шардов распаковывать за раз")
|
||||
ap.add_argument("--readout", action="append", default=[],
|
||||
metavar="ИМЯ=ПУТЬ[@ПОЛЕ=ЗНАЧЕНИЕ...]",
|
||||
help="вариант считывания, после @ — свои поля Params; можно несколько")
|
||||
ap.add_argument("--out", required=True)
|
||||
PS.add_argument(ap)
|
||||
args = ap.parse_args()
|
||||
if not args.readout:
|
||||
raise SystemExit("нужен хотя бы один --readout ИМЯ=ПУТЬ")
|
||||
|
||||
members = ND.pick(ND.shards(args.tar), args.shards)
|
||||
pipes, stats = {}, {}
|
||||
for item in args.readout:
|
||||
name, _, rest = item.partition("=")
|
||||
path, *own = rest.split("@")
|
||||
params = Params(**PS.apply({}, args.set + own, Params))
|
||||
pipes[name] = FlyGuard(params, memory=None, readout=MbonReadout.load(path))
|
||||
stats[name] = dict(frames=0, path_m=0.0, back_m=0.0, alarm_frames=0,
|
||||
fp_objects=0, tracks=set())
|
||||
print(f"шарды {members[0][0]}–{members[-1][0]} ({len(members)} шт.), "
|
||||
f"вариантов считывания {len(pipes)}")
|
||||
|
||||
t0 = time.time()
|
||||
for part in ND.split(members, args.per_chunk):
|
||||
with ND.chunk(args.tar, part) as d:
|
||||
for _, pc in Bag(d).frames():
|
||||
for name, fg in pipes.items():
|
||||
res = fg.process(pc)
|
||||
if res is None:
|
||||
continue
|
||||
s = stats[name]
|
||||
s["frames"] += 1
|
||||
ds = res.ego.ds if res.ego else 0.0
|
||||
s["path_m"] += ds
|
||||
s["back_m"] += max(-ds, 0.0)
|
||||
objs = res.decision.objects
|
||||
if objs:
|
||||
s["alarm_frames"] += 1
|
||||
s["fp_objects"] += len(objs)
|
||||
s["tracks"].update(o.track_id for o in objs)
|
||||
line = " | ".join(f"{n}: {len(s['tracks'])} треков на {s['path_m'] / 1000:.2f} км"
|
||||
for n, s in stats.items())
|
||||
print(f" шарды до {part[-1][0]:3d}: {line} | {(time.time() - t0) / 60:5.1f} мин",
|
||||
flush=True)
|
||||
|
||||
rows = []
|
||||
for name, s in stats.items():
|
||||
km = max(s["path_m"] / 1000.0, 1e-6)
|
||||
rows.append(dict(readout=name, frames=s["frames"], path_m=s["path_m"],
|
||||
back_m=s["back_m"], alarm_frames=s["alarm_frames"],
|
||||
alarm_rate=s["alarm_frames"] / max(s["frames"], 1),
|
||||
fp_objects=s["fp_objects"], fp_tracks=len(s["tracks"]),
|
||||
fp_per_km=len(s["tracks"]) / km))
|
||||
print(f"{name:12s} кадров {s['frames']:6d}, путь {s['path_m'] / 1000:6.2f} км "
|
||||
f"(назад {s['back_m']:.0f} м) | кадров с тревогой {rows[-1]['alarm_rate']:6.1%} | "
|
||||
f"ложных треков {len(s['tracks']):4d} = {rows[-1]['fp_per_km']:5.1f} на км")
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
json.dump(rows, f, ensure_ascii=False, indent=1)
|
||||
print("сохранено:", args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -23,6 +23,11 @@
|
|||
независимой проверкой того, что модель выучила предмет, а не «синтетику».
|
||||
|
||||
python tools/make_training_set.py --out data/cache/training_set.npz
|
||||
|
||||
Выборку можно собрать и из большого бэга `new_data`, кусками прямо из архива
|
||||
(`--new-data 0:110` — первые 110 шардов, по пять на «запись»): двадцать
|
||||
минут езды другим днём дают считыванию обстановку, которой нет в пяти
|
||||
коротких записях (EXPERIMENTS п. 17).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -31,6 +36,7 @@ import argparse
|
|||
import numpy as np
|
||||
|
||||
import _bootstrap as B # noqa: F401
|
||||
import _new_data as ND
|
||||
import _parallel as P
|
||||
import _params as PS
|
||||
from flyguard.bag import Bag, find_bags
|
||||
|
|
@ -43,9 +49,14 @@ MIN_OVERLAP = 0.5 # доля лучей ядра, пришедш
|
|||
|
||||
|
||||
def _work(task):
|
||||
"""Одна задача — один бэг. Верхнего уровня: иначе не передать в процесс."""
|
||||
path, params, limit, d_starts, laterals, seed = task
|
||||
return collect_bag(path, params, limit, d_starts, laterals, seed)
|
||||
"""Одна задача — один бэг или кусок `new_data`. Верхнего уровня: иначе не
|
||||
передать в процесс. Кусок распаковывается внутри воркера и удаляется за ним."""
|
||||
src, params, limit, d_starts, laterals, seed = task
|
||||
if isinstance(src, tuple):
|
||||
tar, members = src
|
||||
with ND.chunk(tar, members) as d:
|
||||
return collect_bag(d, params, limit, d_starts, laterals, seed)
|
||||
return collect_bag(src, params, limit, d_starts, laterals, seed)
|
||||
|
||||
|
||||
def ego_track(bag: Bag, params: Params, limit: int):
|
||||
|
|
@ -141,6 +152,12 @@ def main() -> None:
|
|||
# красивая и бессмысленная, а в тоннеле у оси полно штатных конструкций.
|
||||
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
||||
ap.add_argument("--seed", type=int, default=20260921)
|
||||
ap.add_argument("--new-data", default="",
|
||||
help="срез номеров шардов new_data, например 0:110: собирать "
|
||||
"выборку из кусков большого бэга вместо записей --root")
|
||||
ap.add_argument("--tar", default=ND.DEFAULT_TAR, help="архив new_data")
|
||||
ap.add_argument("--per-chunk", type=int, default=5,
|
||||
help="шардов в куске new_data; кусок считается одной записью")
|
||||
P.add_argument(ap)
|
||||
PS.add_argument(ap)
|
||||
args = ap.parse_args()
|
||||
|
|
@ -150,13 +167,20 @@ def main() -> None:
|
|||
params = Params(**PS.apply({}, args.set, Params))
|
||||
B.CACHE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
||||
tasks = [(p, params, args.limit, d_starts, laterals, args.seed) for p in bags]
|
||||
if args.new_data:
|
||||
chunks = ND.split(ND.pick(ND.shards(args.tar), args.new_data), args.per_chunk)
|
||||
names = [f"new_data_{c[0][0]:03d}_{c[-1][0]:03d}" for c in chunks]
|
||||
sources = [(args.tar, c) for c in chunks]
|
||||
else:
|
||||
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
||||
names = [p.name for p in bags]
|
||||
sources = bags
|
||||
tasks = [(s, params, args.limit, d_starts, laterals, args.seed) for s in sources]
|
||||
# Печатается по готовности, складывается по номеру задачи: порядок бэгов
|
||||
# в файле не должен зависеть от того, какой из них досчитался первым.
|
||||
slots: list = [None] * len(tasks)
|
||||
for i, task, got, secs in P.run(_work, tasks, args.jobs):
|
||||
name = task[0].name
|
||||
name = names[i]
|
||||
if got is None:
|
||||
print(f" {name:42s} пропущен")
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -36,9 +36,17 @@ BANDS = ((0, 30), (30, 55), (55, 80), (80, 110), (110, 160), (160, 230))
|
|||
|
||||
|
||||
def loo(names, X, Y, cfg, n_pn, args, keep_models=False):
|
||||
"""Обучение на всех бэгах кроме проверяемого. Возвращает список результатов."""
|
||||
"""Обучение на всех бэгах кроме проверяемого. Возвращает список результатов.
|
||||
|
||||
Проверяемыми бывают не все записи: куски `new_data` нужны только для
|
||||
обучения, и отдельная складка на каждый из них — лишние полчаса
|
||||
(`--train-only`).
|
||||
"""
|
||||
skip = tuple(p for p in args.train_only.split(",") if p)
|
||||
out = []
|
||||
for held in names:
|
||||
if skip and held.startswith(skip):
|
||||
continue
|
||||
tr = [n for n in names if n != held]
|
||||
Xtr = np.concatenate([X[n] for n in tr])
|
||||
ytr = np.concatenate([Y[n] for n in tr])
|
||||
|
|
@ -53,7 +61,9 @@ def loo(names, X, Y, cfg, n_pn, args, keep_models=False):
|
|||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--data", default=str(B.CACHE / "training_set.npz"))
|
||||
ap.add_argument("--data", action="append", default=None,
|
||||
help="выборка make_training_set; можно несколько — записи "
|
||||
"объединяются (по умолчанию data/cache/training_set.npz)")
|
||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "mbon_readout.npz"))
|
||||
ap.add_argument("--n-kc", type=int, default=0,
|
||||
help="ёмкость итоговой модели; 0 — взять лучшую из развёртки")
|
||||
|
|
@ -61,6 +71,9 @@ def main() -> None:
|
|||
help="ёмкости для развёртки; пусто — не разворачивать")
|
||||
ap.add_argument("--active", type=int, default=100,
|
||||
help="активных клеток после торможения APL")
|
||||
ap.add_argument("--train-only", default="",
|
||||
help="префиксы записей через запятую, которые только учат и "
|
||||
"своей складки не получают, например new_data_")
|
||||
ap.add_argument("--epochs", type=int, default=60)
|
||||
ap.add_argument("--lr", type=float, default=4.0)
|
||||
ap.add_argument("--l2", type=float, default=1e-5)
|
||||
|
|
@ -73,12 +86,20 @@ def main() -> None:
|
|||
"оценка нечестна — считывание увидит проверяемый бэг")
|
||||
args = ap.parse_args()
|
||||
|
||||
d = np.load(args.data, allow_pickle=True)
|
||||
names = [str(n) for n in d["names"]]
|
||||
X = {n: d[f"X_{n}"].astype(np.float32) for n in names}
|
||||
Y = {n: d[f"y_{n}"].astype(np.int8) for n in names}
|
||||
D = {n: d[f"d_{n}"].astype(np.float32) for n in names}
|
||||
feats = [str(f) for f in d["features"]]
|
||||
names, X, Y, D, feats = [], {}, {}, {}, None
|
||||
for path in args.data or [str(B.CACHE / "training_set.npz")]:
|
||||
d = np.load(path, allow_pickle=True)
|
||||
f_here = [str(f) for f in d["features"]]
|
||||
if feats is not None and f_here != feats:
|
||||
raise SystemExit(f"{path}: другой набор признаков — выборки несовместимы")
|
||||
feats = f_here
|
||||
for n in (str(n) for n in d["names"]):
|
||||
if n in X:
|
||||
raise SystemExit(f"запись {n} встречается в двух выборках")
|
||||
names.append(n)
|
||||
X[n] = d[f"X_{n}"].astype(np.float32)
|
||||
Y[n] = d[f"y_{n}"].astype(np.int8)
|
||||
D[n] = d[f"d_{n}"].astype(np.float32)
|
||||
n_pn = next(iter(X.values())).shape[1]
|
||||
tot = sum(v.shape[0] for v in X.values())
|
||||
pos = sum(int(v.sum()) for v in Y.values())
|
||||
|
|
|
|||
Loading…
Reference in a new issue