forked from Dan4ick/Lidar_Muxa
Обученное считывание MBON: ложных тревог вдвое меньше
This commit is contained in:
parent
b8e95eccbe
commit
8778bae632
15 changed files with 597 additions and 82 deletions
22
README.md
22
README.md
|
|
@ -87,7 +87,7 @@ flyguard/ ядро: стадии обработки, память, сч
|
||||||
pipeline.py сборка
|
pipeline.py сборка
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор
|
tools/ обучение, оценка, разбор
|
||||||
tests/ 27 тестов, запускаются без данных и без ROS
|
tests/ 28 тестов, запускаются без данных и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
@ -114,8 +114,9 @@ set FLYGUARD_DATA=D:\lidar\data
|
||||||
python tools/make_training_set.py # размеченная выборка
|
python tools/make_training_set.py # размеченная выборка
|
||||||
python tools/train_mbon.py --device cuda --baseline # считывание MBON
|
python tools/train_mbon.py --device cuda --baseline # считывание MBON
|
||||||
python tools/evaluate.py --mbon-dir artifacts/mbon_folds # ложные тревоги
|
python tools/evaluate.py --mbon-dir artifacts/mbon_folds # ложные тревоги
|
||||||
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz
|
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
|
||||||
python tools/plot_benchmark.py # дальность обнаружения
|
--mbon-dir artifacts/mbon_folds # дальность обнаружения
|
||||||
|
python tools/plot_benchmark.py # кривые и график
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -124,12 +125,14 @@ python tools/plot_benchmark.py # дальность обн
|
||||||
|
|
||||||
| Метрика | Значение | Чем измерено |
|
| Метрика | Значение | Чем измерено |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Реальный объект 0.67 × 1.35 м на 55 м | **98.9 %** кадров | `tools/check_obstacle.py` |
|
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
||||||
| Ложные тревоги, leave-one-bag-out | **7.5 трека на км**, 16.7 % кадров | `tools/evaluate.py` |
|
| Ложные тревоги, leave-one-bag-out | **3.3 трека на км**, 4.5 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||||
|
| То же без обученного считывания | 7.4 на км, 16.7 % кадров | там же, без `--mbon-dir` |
|
||||||
| То же на новой линии, без обученной памяти | 21.8 на км | там же, холодный старт |
|
| То же на новой линии, без обученной памяти | 21.8 на км | там же, холодный старт |
|
||||||
| Рабочая дальность (полигон, 14 004 наблюдения) | **100 м** человек стоя, P@50 = 0.70 | `tools/plot_benchmark.py` |
|
| Рабочая дальность (полигон, 14 004 наблюдения) | **80 м** человек стоя и сидя, P@50 = 0.70, P@100 = 0.31 | `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.986** | `tools/train_mbon.py` |
|
||||||
|
|
||||||
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
||||||
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
||||||
|
|
@ -150,6 +153,11 @@ python tools/plot_benchmark.py # дальность обн
|
||||||
полотну, и самая низкая видимая точка у оси пути оказывается выше головки
|
полотну, и самая низкая видимая точка у оси пути оказывается выше головки
|
||||||
рельса на 0.1–0.6 м.
|
рельса на 0.1–0.6 м.
|
||||||
* Привыкание внутри проезда сделано и **отвергнуто замером** — п. 10.
|
* Привыкание внутри проезда сделано и **отвергнуто замером** — п. 10.
|
||||||
|
* **Яркость как признак мы себе не засчитываем.** Абсолютной шкалы
|
||||||
|
интенсивности в записях нет: медиана по кандидатам обстановки 3…7 в пяти
|
||||||
|
бэгах и 23.5 в шестом. Вставка берёт яркость реальных возвратов с тех же
|
||||||
|
лучей, то есть признак намеренно обесточен, и дальность из-за этого
|
||||||
|
занижена — настоящий предмет был в 1.47 раза ярче окружения. Разбор — п. 11.1.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -9,45 +9,30 @@
|
||||||
"fp_tracks": 5,
|
"fp_tracks": 5,
|
||||||
"fp_per_km": NaN,
|
"fp_per_km": NaN,
|
||||||
"fp_median_d": 76.65631835070762,
|
"fp_median_d": 76.65631835070762,
|
||||||
"obj_rate": 0.9894736842105263,
|
"obj_rate": 0.9947368421052631,
|
||||||
"ms_p50": 33.958500005610404,
|
"ms_p50": 33.38889999940875,
|
||||||
"ms_p95": 37.71089500642119,
|
"ms_p95": 37.88361000169971,
|
||||||
"train_size": 37073
|
"train_size": 37073
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "doubleT_platform",
|
"bag": "doubleT_platform",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.74905739412534,
|
"path_m": 200.74905739412534,
|
||||||
"alarm_frames": 56,
|
"alarm_frames": 28,
|
||||||
"alarm_rate": 0.23430962343096234,
|
"alarm_rate": 0.11715481171548117,
|
||||||
"fp_objects": 56,
|
"fp_objects": 28,
|
||||||
"fp_tracks": 1,
|
"fp_tracks": 1,
|
||||||
"fp_per_km": 4.981343439320496,
|
"fp_per_km": 4.981343439320496,
|
||||||
"fp_median_d": 62.79671678753603,
|
"fp_median_d": 33.983066409463774,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": 40.0374000055308,
|
"ms_p50": 39.082800001779106,
|
||||||
"ms_p95": 43.40603000018746,
|
"ms_p95": 45.35096999898087,
|
||||||
"train_size": 36151
|
"train_size": 36151
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "roundT_doubleT",
|
"bag": "roundT_doubleT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.9598713551979,
|
"path_m": 200.9598713551979,
|
||||||
"alarm_frames": 3,
|
|
||||||
"alarm_rate": 0.012552301255230125,
|
|
||||||
"fp_objects": 3,
|
|
||||||
"fp_tracks": 1,
|
|
||||||
"fp_per_km": 4.976117835149752,
|
|
||||||
"fp_median_d": 59.10944286836971,
|
|
||||||
"obj_rate": null,
|
|
||||||
"ms_p50": 39.84129999298602,
|
|
||||||
"ms_p95": 49.274240005979664,
|
|
||||||
"train_size": 36584
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bag": "roundT_pressureGate_roundT",
|
|
||||||
"frames": 239,
|
|
||||||
"path_m": 246.7637153487153,
|
|
||||||
"alarm_frames": 0,
|
"alarm_frames": 0,
|
||||||
"alarm_rate": 0.0,
|
"alarm_rate": 0.0,
|
||||||
"fp_objects": 0,
|
"fp_objects": 0,
|
||||||
|
|
@ -55,38 +40,53 @@
|
||||||
"fp_per_km": 0.0,
|
"fp_per_km": 0.0,
|
||||||
"fp_median_d": NaN,
|
"fp_median_d": NaN,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": 46.89270000017132,
|
"ms_p50": 37.19460000138497,
|
||||||
"ms_p95": 52.964819991757395,
|
"ms_p95": 53.73476000095251,
|
||||||
|
"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,
|
||||||
|
"obj_rate": null,
|
||||||
|
"ms_p50": 45.79000000376254,
|
||||||
|
"ms_p95": 48.463629999241675,
|
||||||
"train_size": 36718
|
"train_size": 36718
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "roundT_squareT_pressureGate_squareT",
|
"bag": "roundT_squareT_pressureGate_squareT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 273.9439574444854,
|
"path_m": 273.9439574444854,
|
||||||
"alarm_frames": 86,
|
"alarm_frames": 13,
|
||||||
"alarm_rate": 0.3598326359832636,
|
"alarm_rate": 0.05439330543933055,
|
||||||
"fp_objects": 89,
|
"fp_objects": 13,
|
||||||
"fp_tracks": 3,
|
"fp_tracks": 1,
|
||||||
"fp_per_km": 10.951145000553437,
|
"fp_per_km": 3.6503816668511457,
|
||||||
"fp_median_d": 108.44065443448855,
|
"fp_median_d": 95.9543811760172,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": 46.32040000797133,
|
"ms_p50": 46.54909999590018,
|
||||||
"ms_p95": 50.264649999007815,
|
"ms_p95": 49.73449000317487,
|
||||||
"train_size": 36083
|
"train_size": 36083
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "squareT_platform_squareT_switch",
|
"bag": "squareT_platform_squareT_switch",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 270.98139848673645,
|
"path_m": 270.98139848673645,
|
||||||
"alarm_frames": 34,
|
"alarm_frames": 9,
|
||||||
"alarm_rate": 0.14225941422594143,
|
"alarm_rate": 0.03765690376569038,
|
||||||
"fp_objects": 34,
|
"fp_objects": 9,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 1,
|
||||||
"fp_per_km": 7.380580405772363,
|
"fp_per_km": 3.6902902028861817,
|
||||||
"fp_median_d": 98.006467832879,
|
"fp_median_d": 96.63277916999479,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": 40.29609999633976,
|
"ms_p50": 41.41209999579587,
|
||||||
"ms_p95": 47.709809999651036,
|
"ms_p95": 46.84415000701847,
|
||||||
"train_size": 33658
|
"train_size": 33658
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
BIN
artifacts/mbon_readout.npz
Normal file
BIN
artifacts/mbon_readout.npz
Normal file
Binary file not shown.
|
|
@ -968,3 +968,251 @@ MBON-α′3, а момент депрессии разрешает коорди
|
||||||
параметрами: ёмкость, квантиль отсчёта, огрубление, шаг «разных мест», путь
|
параметрами: ёмкость, квантиль отсчёта, огрубление, шаг «разных мест», путь
|
||||||
полузабывания. На линии, где обстановка стандартизована сильнее, чем в этих записях,
|
полузабывания. На линии, где обстановка стандартизована сильнее, чем в этих записях,
|
||||||
он может заработать — но проверять это надо замером на той линии, а не надеждой.
|
он может заработать — но проверять это надо замером на той линии, а не надеждой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Обученное считывание MBON: метки из физики
|
||||||
|
|
||||||
|
До сих пор грибовидное тело училось **без учителя**: оно запоминало частоту
|
||||||
|
обстановки и отвечало «знакомо/ново». Вставка предметов трассировкой лучей даёт
|
||||||
|
то, чего в датасете нет, — **метку**. `synth.inject` возвращает индексы лучей,
|
||||||
|
которые он переписал, поэтому кандидат размечается точным пересечением: предмет,
|
||||||
|
если не меньше половины его лучей пришли от вставки (`tools/make_training_set.py`,
|
||||||
|
`MIN_OVERLAP = 0.5`). Слои остаются те же — разрежённый код клеток Кеньона,
|
||||||
|
торможение APL, один выход MBON, — меняется только учитель: вместо частоты
|
||||||
|
обстановки различение «предмет / тоннель» (`flyguard/mbon_readout.py`).
|
||||||
|
|
||||||
|
### 11.1. Сначала — найденный артефакт: интенсивность выдавала вставку
|
||||||
|
|
||||||
|
Первое же обучение дало подозрительно красивые цифры: leave-one-bag-out AUC
|
||||||
|
**0.9935**, по полосам дальности от 0.998 вблизи до 0.941 на 160–230 м. Разбор
|
||||||
|
важности перестановкой поставил наверх не форму, а **интенсивность**:
|
||||||
|
|
||||||
|
```
|
||||||
|
inten +0.0187 ← втрое больше следующего
|
||||||
|
elongation +0.0051
|
||||||
|
fill +0.0048
|
||||||
|
log_rays +0.0030
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка показала, откуда это взялось. Интенсивность вставки считалась по
|
||||||
|
ламбертовой модели ρ·cosθ/r², и произведение `intensity · r²` у вставленных
|
||||||
|
предметов держалось в пределах **1.8×** между p10 и p90, тогда как у настоящих
|
||||||
|
кандидатов разброс **64×**. Иначе говоря, вставка — и только она — точно
|
||||||
|
подчинялась учебниковому закону обратных квадратов, и это распознаётся надёжнее
|
||||||
|
любой формы.
|
||||||
|
|
||||||
|
Хуже того, у нижнего среза шкалы признак переворачивается. Разделяющая
|
||||||
|
способность **одной интенсивности** по полосам:
|
||||||
|
|
||||||
|
| полоса | intensity вставки | intensity обстановки | AUC по одной интенсивности |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 0–30 м | 37.2 | 5.0 | 0.956 |
|
||||||
|
| 30–55 м | 4.6 | 4.5 | 0.513 |
|
||||||
|
| 55–80 м | 1.7 | 4.0 | 0.287 |
|
||||||
|
| 80–110 м | 1.0 | 3.0 | 0.197 |
|
||||||
|
| 110–160 м | 1.0 | 5.0 | **0.049** |
|
||||||
|
| 160–230 м | 1.0 | 7.5 | **0.010** |
|
||||||
|
|
||||||
|
За 110 м расчётная яркость вставки падала ниже единицы, упиралась в срез шкалы —
|
||||||
|
и «тускло» становилось почти безошибочным признаком предмета. AUC 0.010 означает
|
||||||
|
почти идеальное разделение с обратным знаком.
|
||||||
|
|
||||||
|
**В реальных данных всё наоборот.** Медиана интенсивности по всему облаку,
|
||||||
|
20 кадров на бэг:
|
||||||
|
|
||||||
|
| бэг | 5–15 м | 15–30 | 30–50 | 50–80 | 80–120 | 120–170 | 170–230 |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| `doubleT_obstacle` | 11 | 9 | 8 | 9 | 6 | 6 | 6 |
|
||||||
|
| `roundT_doubleT` | 10 | 8 | 8 | 4 | 2 | 3 | 4 |
|
||||||
|
| `squareT_platform_squareT_switch` | 8 | 8 | 7 | 3 | 2 | 2 | 4 |
|
||||||
|
|
||||||
|
Никакого 1/r². Прибор отдаёт не принятую энергию, а **отражательную способность
|
||||||
|
с компенсацией дальности** — то, что в руководстве Hesai называется
|
||||||
|
reflectivity. Ламбертова модель описывала физику фотона, а не то число, которое
|
||||||
|
лежит в поле `intensity`.
|
||||||
|
|
||||||
|
Настоящий предмет даёт точку привязки. В `doubleT_obstacle` он стоит на 55.9 м,
|
||||||
|
57 лучей, и его медианная интенсивность — **34.5** (p10…p90 = 31…39) при
|
||||||
|
медиане окружающих кандидатов 23.5. Старая формула на этой дальности дала бы
|
||||||
|
**1.45**, то есть в 24 раза тусклее настоящего.
|
||||||
|
|
||||||
|
**Первая попытка починки была неправильной.** Раз прибор компенсирует
|
||||||
|
дальность, яркость вставки сделали постоянной: `100 · ρ · √cosθ` со шкалой,
|
||||||
|
привязанной к настоящему предмету (ρ = 0.35 → около 34). Выборка пересобрана,
|
||||||
|
модель переобучена — и артефакт просто перевернулся:
|
||||||
|
|
||||||
|
| полоса | вставка | обстановка | AUC по одной интенсивности |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| 0–30 м | 35.0 | 5.0 | 0.966 |
|
||||||
|
| 30–55 м | 35.2 | 4.5 | 0.960 |
|
||||||
|
| 55–80 м | 34.9 | 4.0 | 0.959 |
|
||||||
|
| 80–110 м | 35.1 | 3.0 | 0.970 |
|
||||||
|
| 110–160 м | 34.8 | 5.0 | 0.832 |
|
||||||
|
| 160–230 м | 34.2 | 7.5 | 0.798 |
|
||||||
|
|
||||||
|
Важность интенсивности при этом выросла втрое (+0.0533 против +0.0187), то есть
|
||||||
|
модель стала опираться на подсказку ещё сильнее.
|
||||||
|
|
||||||
|
**Настоящая причина: абсолютной шкалы в этих данных нет.** Медиана яркости
|
||||||
|
кандидатов обстановки по бэгам — 3.0, 4.0, 4.0, 4.0, 7.0, а в
|
||||||
|
`doubleT_obstacle`, где лежит настоящий предмет, 23.5 при 34.5 у самого
|
||||||
|
предмета. Разброс между записями впятеро больше, чем контраст предмета к фону
|
||||||
|
внутри записи. Любое абсолютное число, назначенное вставке, оказывается
|
||||||
|
подарком детектору — в ту или в другую сторону.
|
||||||
|
|
||||||
|
**Как сделано в итоге** (`synth.local_intensity` и `synth.IntensityEnv`):
|
||||||
|
вставка берёт яркость **реальных возвратов с тех же самых лучей** — того, что
|
||||||
|
предмет заслонил. Лучи, ушедшие в пустоту (за 100 м таких большинство),
|
||||||
|
получают образец из реальных возвратов **той же полосы дальности** в этом же
|
||||||
|
кадре; разложение кадра по дальности считается один раз и переиспользуется
|
||||||
|
всеми 135 сценариями. Ни отражательной способности, ни закона яркости от
|
||||||
|
дальности в функции нет; ρ осталась там, где ей место, — в вероятности, что
|
||||||
|
эхо вообще вернётся. Проверка вставками в реальные бэги:
|
||||||
|
|
||||||
|
| дальность | 30 м | 60 м | 100 м | 150 м | 200 м |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| вставка | 8.8 | 6.9 | 4.3 | 4.3 | 10.1 |
|
||||||
|
| реальные возвраты рядом | 10.0 | 4.0 | 2.0 | 3.0 | 3.0 |
|
||||||
|
|
||||||
|
Это **заниженная** оценка: настоящий предмет в записи был в 1.47 раза ярче
|
||||||
|
окружения, и этот запас мы себе не засчитываем. На линии с откалиброванной
|
||||||
|
яркостью его можно вернуть — замером, а не верой. Тест
|
||||||
|
`test_injected_intensity_copies_the_surroundings` держит свойство: среди
|
||||||
|
аргументов нет отражения, а без разложения окружения дальность ничего не меняет.
|
||||||
|
|
||||||
|
Замечание, важное для оценки ущерба: складка, не видевшая `doubleT_obstacle`,
|
||||||
|
и с артефактом ставила настоящему предмету **1.000** (p10 = 0.998) против 0.393
|
||||||
|
у обстановки — на ближней дальности модель опиралась не только на подсказку.
|
||||||
|
Но полигонная дальность, измеренная на таких вставках, мерила бы признак,
|
||||||
|
которого в реальности нет. Поэтому все цифры ниже получены на выборке, где
|
||||||
|
яркость взята из самой записи.
|
||||||
|
|
||||||
|
Отдельный урок про метрику: таблицы выше потребовали починить подсчёт AUC.
|
||||||
|
Ранги раздавались по порядку в массиве, а не усреднялись на совпадениях, и
|
||||||
|
признак, у которого все значения равны, получал 0 или 1 вместо 0.5. Первым
|
||||||
|
ложным сигналом такого рода оказался контраст к фону за 160 м — там он
|
||||||
|
структурно равен нулю у всех кандидатов сразу. Теперь ранги средние, а сторож
|
||||||
|
«что делит выборку само по себе» встроен в `tools/train_mbon.py` и печатается
|
||||||
|
перед каждым обучением.
|
||||||
|
|
||||||
|
### 11.2. Что даёт обученное считывание
|
||||||
|
|
||||||
|
Обучение — полнопакетный логистический спуск по разрежённому коду: 4 000
|
||||||
|
клеток Кеньона, 100 активных после торможения APL, 23 признака кандидата,
|
||||||
|
185 252 примера, 19 445 из них предметы. Ёмкость выбрана развёрткой: 8 000 и
|
||||||
|
20 000 клеток дают ту же AUC (0.9859 и 0.9850 против 0.9860), а в худшем кадре
|
||||||
|
стоят 2.8 и 7.3 мс вместо 1.5 — платить не за что.
|
||||||
|
|
||||||
|
Проверка — leave-one-bag-out, отдельная модель на каждую складку. Без этого
|
||||||
|
сквозные цифры были бы ложными: считывание увидело бы проверяемый бэг ровно
|
||||||
|
так же, как память тоннеля, обученная на всём подряд.
|
||||||
|
|
||||||
|
| бэг | AUC | обстановки при 95 % предметов | бустинг по сырым признакам |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| `doubleT_platform` | 0.9923 | 3.54 % | 0.9951 |
|
||||||
|
| `roundT_doubleT` | 0.9939 | 2.79 % | 0.9987 |
|
||||||
|
| `roundT_pressureGate_roundT` | 0.9759 | 16.30 % | 0.9866 |
|
||||||
|
| `roundT_squareT_pressureGate_squareT` | 0.9828 | 11.65 % | 0.9930 |
|
||||||
|
| `squareT_platform_squareT_switch` | 0.9851 | 5.87 % | 0.9902 |
|
||||||
|
| **среднее** | **0.9860** | **8.03 %** | 0.9927 |
|
||||||
|
|
||||||
|
AUC по полосам дальности: 0.995 / 0.996 / 0.992 / 0.973 / 0.955 / 0.738 —
|
||||||
|
качество честно падает с дальностью, как и должно быть, когда от предмета
|
||||||
|
остаётся четыре луча.
|
||||||
|
|
||||||
|
Градиентный бустинг по тем же сырым признакам стабильно лучше на 0.005…0.01
|
||||||
|
AUC. Это цена разрежённого кода, и мы её знаем: взамен получаем схему, которая
|
||||||
|
считается за 1.5 мс на худшем кадре и держит непрерывное дообучение.
|
||||||
|
|
||||||
|
Наверху важности — форма, а не положение и не яркость:
|
||||||
|
|
||||||
|
```
|
||||||
|
fill +0.0079 abs_lat +0.0051
|
||||||
|
elongation +0.0076 acc_support +0.0046
|
||||||
|
h +0.0055 lat +0.0044
|
||||||
|
```
|
||||||
|
|
||||||
|
Положение в сечении (`abs_lat`, `lat`) стоит на 4–6 месте. Формально это
|
||||||
|
законный признак — опасны именно предметы в габарите, — но границу
|
||||||
|
«положительной» области задали наши же расстановки (0, ±0.6, ±1.2 м от оси),
|
||||||
|
и на защите это надо называть вслух, а не прятать.
|
||||||
|
|
||||||
|
**Сквозной результат, leave-one-bag-out:**
|
||||||
|
|
||||||
|
| | без считывания | со считыванием |
|
||||||
|
|---|---:|---:|
|
||||||
|
| разных ложных треков на километр | 7.5 | **3.3** |
|
||||||
|
| кадров с ложной тревогой | 16.7 % | **4.4 %** |
|
||||||
|
| реальный объект на 55 м | 98.9 % | **98.9 %** |
|
||||||
|
| задержка кадра, медиана | 33 мс | 35–41 мс |
|
||||||
|
|
||||||
|
**На размеченном полигоне** (модель по складкам, то есть для каждого бэга та,
|
||||||
|
что его не видела):
|
||||||
|
|
||||||
|
| человек стоя | без считывания | со считыванием |
|
||||||
|
|---|---:|---:|
|
||||||
|
| рабочая дальность «как есть» | 62 м | **80 м** |
|
||||||
|
| P@50 м | 0.70 | 0.70 |
|
||||||
|
| P@100 м | 0.19 | **0.31** |
|
||||||
|
| посторонних тревог на кадр | 0.169 | **0.030** |
|
||||||
|
|
||||||
|
Прибавка не только на человеке: ящик P@100 0.02 → 0.24, человек сидя
|
||||||
|
0.25 → 0.39, чемодан P@50 0.55 → 0.60, ведро впервые поднялось над нулём
|
||||||
|
(рабочая дальность 8 → 20 м).
|
||||||
|
|
||||||
|
Отдельно: прежние заявленные 100 м рабочей дальности и P@100 = 0.53 получены
|
||||||
|
на полигоне с артефактом яркости и **завышены**. Честные числа без считывания —
|
||||||
|
80 м и 0.19, со считыванием — 80 м и 0.31.
|
||||||
|
|
||||||
|
### 11.3. Сколько дальности покупается за ложные тревоги
|
||||||
|
|
||||||
|
Смысл обученного считывания не в том, чтобы просто показать меньшее число
|
||||||
|
ложных. Оно даёт **запас**, а запас надо тратить: поезд движется, и дальность
|
||||||
|
обнаружения — это время на торможение. Поэтому пороги решения вынесены в
|
||||||
|
`Params` и развёрнуты замером. Все строки — leave-one-bag-out по ложным и
|
||||||
|
полигон по дальности, модель по складкам:
|
||||||
|
|
||||||
|
| конфигурация | ложных треков/км | кадров с тревогой | реальный объект | P@50 | P@100 | тревог на кадр, полигон |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| без считывания | 7.4 | 16.65 % | 98.9 % | 0.70 | 0.19 | 0.169 |
|
||||||
|
| считывание, три подтверждения | 3.3 | 4.44 % | 98.9 % | 0.70 | 0.31 | 0.030 |
|
||||||
|
| **по умолчанию: считывание, два подтверждения** | **3.3** | **4.52 %** | **99.5 %** | **0.70** | **0.31** | **0.030** |
|
||||||
|
| тише: смешивание 0.7 | 2.5 | 4.85 % | 98.9 % | 0.70 | 0.23 | 0.016 |
|
||||||
|
| дальше: порог улики 0.40 | 5.3 | 6.19 % | 98.9 % | 0.71 | 0.34 | 0.039 |
|
||||||
|
| смешивание 0.7 + порог 0.32 | 4.3 | 7.28 % | 98.9 % | 0.71 | 0.31 | 0.055 |
|
||||||
|
| порог 0.30 | 7.8 | 8.37 % | — | — | — | — |
|
||||||
|
| порог 0.22 | 9.8 | 10.04 % | — | — | — | — |
|
||||||
|
|
||||||
|
Три вывода, каждый замером.
|
||||||
|
|
||||||
|
**Смешивание с ручной формулой — не способ купить дальность.** Строка
|
||||||
|
«смешивание 0.7 + порог 0.32» даёт ровно ту же P@100 = 0.31, что и чистая
|
||||||
|
модель при штатном пороге, но стоит 4.3 ложных трека вместо 3.3 и вдвое
|
||||||
|
больше посторонних тревог на полигоне. Смешивание полезно в другом качестве —
|
||||||
|
как **тихий режим**: при штатном пороге оно опускает ложные до 2.5 на км и
|
||||||
|
0.016 на кадр, отдавая дальность (P@100 0.31 → 0.23). Оно же — страховка на
|
||||||
|
случай, когда предмет не похож ни на что из каталога: ручная формула
|
||||||
|
продолжает работать по геометрии.
|
||||||
|
|
||||||
|
**Понижение порога работает, но дорого.** 0.50 → 0.40 добавляет 0.03 к P@100
|
||||||
|
человека и 0.07 ящику, а платит 3.3 → 5.3 ложных трека. Дальше кривая только
|
||||||
|
круче: 0.30 стоит 7.8, 0.22 — 9.8, то есть хуже, чем было без считывания
|
||||||
|
вообще. Оставлено пресетом, а не умолчанием.
|
||||||
|
|
||||||
|
**Два подтверждения вместо трёх оказались бесплатными.** Ложные не изменились
|
||||||
|
(3.3 на км, 4.44 → 4.52 % кадров), посторонние тревоги на полигоне не
|
||||||
|
изменились тоже (0.030 на кадр), а обнаружение выросло: реальный объект
|
||||||
|
98.9 → **99.5 %**, рабочая дальность человека сидя 20 → **80 м**, чемодана
|
||||||
|
«как есть» 20 → **62 м**. Причина видна из данных: на 60…100 м предмет
|
||||||
|
среднего размера виден через кадр, и третьего подтверждения приходится ждать
|
||||||
|
дольше, чем он успевает приблизиться. Это стало умолчанием.
|
||||||
|
|
||||||
|
Строки пресетов измерены при трёх подтверждениях — прежнем умолчании. В
|
||||||
|
базовой точке переход на два ложных не изменил, но для пресетов это отдельно
|
||||||
|
не перемерялось.
|
||||||
|
|
||||||
|
**Чего порогами не изменить.** P@150 равна нулю в любой конфигурации. Это не
|
||||||
|
алгоритм: прямая видимость в этих тоннелях 121–167 м, а прирельсовая зона за
|
||||||
|
80–90 м не наблюдается вовсе — луч скользит по полотну (п. 3 и п. 7.3).
|
||||||
|
Потолок здесь геометрический.
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class DescendingNeurons:
|
||||||
"""Два порога с гистерезисом поверх подтверждённых треков."""
|
"""Два порога с гистерезисом поверх подтверждённых треков."""
|
||||||
|
|
||||||
def __init__(self, *, warn_evidence: float = 0.5, clear_evidence: float = 0.3,
|
def __init__(self, *, warn_evidence: float = 0.5, clear_evidence: float = 0.3,
|
||||||
emergency_evidence: float = 0.75, min_hits: int = 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):
|
||||||
self.warn_evidence = warn_evidence
|
self.warn_evidence = warn_evidence
|
||||||
self.clear_evidence = clear_evidence
|
self.clear_evidence = clear_evidence
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,20 @@ class Params:
|
||||||
enable_mbon: bool = True
|
enable_mbon: bool = True
|
||||||
mbon_power: float = 1.0 # резкость: p**power перед смешиванием
|
mbon_power: float = 1.0 # резкость: p**power перед смешиванием
|
||||||
mbon_blend: float = 1.0 # 1 — только модель, 0 — только ручная формула
|
mbon_blend: float = 1.0 # 1 — только модель, 0 — только ручная формула
|
||||||
|
# Пороги решения (см. descending.py). Вынесены сюда, потому что обученное
|
||||||
|
# считывание меняет цену улики: когда ложные тревоги падают, запас можно
|
||||||
|
# потратить на дальность, опустив порог. Замер — EXPERIMENTS п. 11.
|
||||||
|
warn_evidence: float = 0.5 # улика, с которой трек поднимает тревогу
|
||||||
|
clear_evidence: float = 0.3 # и до которой держится (гистерезис)
|
||||||
|
emergency_evidence: float = 0.75
|
||||||
|
# Наблюдений, без которых трек не считается. Три подтверждения стоили
|
||||||
|
# дальности на предметах среднего размера: на 60…100 м они видны через
|
||||||
|
# кадр, и третьего приходится ждать слишком долго. Два подтверждения
|
||||||
|
# подняли рабочую дальность человека сидя с 20 до 80 м, а чемодана —
|
||||||
|
# с 20 до 62 м, и не стоили ни одного лишнего ложного трека
|
||||||
|
# (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3.
|
||||||
|
min_hits: int = 2
|
||||||
|
novelty_floor: float = 0.10
|
||||||
d_min: float = 4.0
|
d_min: float = 4.0
|
||||||
d_max: float = 220.0
|
d_max: float = 220.0
|
||||||
min_rays: int = 4
|
min_rays: int = 4
|
||||||
|
|
@ -153,9 +167,14 @@ class FlyGuard:
|
||||||
mbon_power=self.p.mbon_power,
|
mbon_power=self.p.mbon_power,
|
||||||
mbon_blend=self.p.mbon_blend)
|
mbon_blend=self.p.mbon_blend)
|
||||||
# без накопления улик трек подтверждается первым же наблюдением
|
# без накопления улик трек подтверждается первым же наблюдением
|
||||||
self.dn = (DescendingNeurons() if self.p.use_tracking
|
self.dn = (DescendingNeurons(
|
||||||
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
warn_evidence=self.p.warn_evidence,
|
||||||
emergency_evidence=0.0, min_hits=1))
|
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
|
||||||
|
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
||||||
|
emergency_evidence=0.0, min_hits=1))
|
||||||
self._calib: list[PointCloud2] = []
|
self._calib: list[PointCloud2] = []
|
||||||
self.frames_seen = 0
|
self.frames_seen = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@
|
||||||
через конвейер проходит ровно тот же путь, что и настоящие данные, начиная
|
через конвейер проходит ровно тот же путь, что и настоящие данные, начиная
|
||||||
с ретины.
|
с ретины.
|
||||||
|
|
||||||
Модель сенсора намеренно пессимистична: добавляется шум дальности, вероятность
|
Модель сенсора намеренно пессимистична: добавляется шум дальности, а
|
||||||
несостоявшегося эха растёт с расстоянием и с углом падения, а интенсивность
|
вероятность несостоявшегося эха растёт с расстоянием и с углом падения. Лучше
|
||||||
считается по ламбертовой модели. Лучше недооценить свой детектор, чем на
|
недооценить свой детектор, чем на защите обнаружить, что полигон был слишком
|
||||||
защите обнаружить, что полигон был слишком добрым.
|
добрым. Яркость вставки берётся из самой записи — из возвратов на тех же
|
||||||
|
лучах: абсолютной шкалы интенсивности в этих данных нет, и любое назначенное
|
||||||
|
число делает вставку узнаваемой (см. длинный комментарий ниже).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -181,6 +183,101 @@ BEAM_DIV_V = 2.18e-3 # рад
|
||||||
|
|
||||||
_CHANNEL_RANGE: np.ndarray | None = None
|
_CHANNEL_RANGE: np.ndarray | None = None
|
||||||
|
|
||||||
|
# Интенсивность вставки. Здесь нельзя придумать ни формулы, ни числа.
|
||||||
|
#
|
||||||
|
# Сначала стояла ламбертова ρ·cosθ/r². Прибор, однако, отдаёт не принятую
|
||||||
|
# энергию, а отражательную способность с компенсацией дальности: медиана по
|
||||||
|
# облаку держится 8…9 от 5 до 50 м и как 1/r² не падает. Вставка получалась на
|
||||||
|
# 55 м в двадцать раз тусклее настоящего предмета, а за 110 м упиралась в
|
||||||
|
# нижний срез шкалы — и «тускло» становилось безошибочным признаком предмета.
|
||||||
|
#
|
||||||
|
# Замена на постоянную яркость 100·ρ·√cosθ, привязанную к настоящему предмету
|
||||||
|
# (34.5 на 55.9 м), просто перевернула артефакт: 35 против 3…7 у обстановки на
|
||||||
|
# каждой полосе дальности, AUC по одной интенсивности 0.96…0.97.
|
||||||
|
#
|
||||||
|
# Причина в том, что абсолютной шкалы тут нет. Медиана яркости кандидатов
|
||||||
|
# обстановки по бэгам: 3.0, 4.0, 4.0, 4.0, 7.0 — а в `doubleT_obstacle`, где
|
||||||
|
# лежит настоящий предмет, 23.5 при 34.5 у самого предмета. Разница между
|
||||||
|
# записями впятеро больше, чем контраст предмета к фону внутри записи. Любое
|
||||||
|
# абсолютное число, назначенное вставке, оказывается подарком детектору — в ту
|
||||||
|
# или в другую сторону.
|
||||||
|
#
|
||||||
|
# Поэтому вставка берёт яркость **реальных возвратов с тех же самых лучей** —
|
||||||
|
# того, что предмет заслонил; где эха не было, из возвратов вдоль остальных его
|
||||||
|
# лучей. Признак становится неинформативным, и полигон меряет геометрию и
|
||||||
|
# движение, то есть то, что мы моделируем честно. Оценка заниженная: настоящий
|
||||||
|
# предмет в записи был в 1.47 раза ярче окружения. На линии с откалиброванной
|
||||||
|
# яркостью этот запас можно вернуть — замером, а не верой.
|
||||||
|
#
|
||||||
|
# Отражательная способность никуда не делась: она определяет, вернётся ли эхо
|
||||||
|
# вообще (`dropout_probability`), а это и есть её настоящая роль.
|
||||||
|
INTEN_SPREAD = 0.12 # разброс отсчёта, логнормальный, ≈ ±12 %
|
||||||
|
|
||||||
|
|
||||||
|
class IntensityEnv:
|
||||||
|
"""Яркость реальных возвратов кадра, разложенная по дальности.
|
||||||
|
|
||||||
|
Нужна, чтобы луч предмета, ушедший в пустоту, получил яркость такую же, как
|
||||||
|
у настоящих возвратов С ТОЙ ЖЕ дальности, а не с ближних и ярких. Считается
|
||||||
|
один раз на кадр и переиспользуется всеми сценариями: в сборе выборки их
|
||||||
|
135 на кадр, и пересчитывать корни по полутора миллионам точек для каждого
|
||||||
|
незачем.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EDGES = np.array([0, 20, 40, 60, 90, 130, 180, 260], np.float32)
|
||||||
|
|
||||||
|
def __init__(self, pc: PointCloud2):
|
||||||
|
q = pc.points
|
||||||
|
it = np.asarray(q["intensity"], np.float32)
|
||||||
|
x, y, z = q["x"], q["y"], q["z"]
|
||||||
|
r2 = x * x + y * y + z * z
|
||||||
|
ok = (it > 0) & (r2 > 1.0) & np.isfinite(r2)
|
||||||
|
r = np.sqrt(r2[ok], dtype=np.float32)
|
||||||
|
v = it[ok]
|
||||||
|
b = np.clip(np.searchsorted(self.EDGES, r, side="right") - 1,
|
||||||
|
0, self.EDGES.size - 2)
|
||||||
|
order = np.argsort(b, kind="stable")
|
||||||
|
b_s, v_s = b[order], v[order]
|
||||||
|
cut = np.searchsorted(b_s, np.arange(self.EDGES.size - 1), side="left")
|
||||||
|
cut = np.append(cut, b_s.size)
|
||||||
|
self._pools = [v_s[cut[i]:cut[i + 1]] for i in range(self.EDGES.size - 1)]
|
||||||
|
self._all = v_s
|
||||||
|
|
||||||
|
def sample(self, d: float, n: int, rng: np.random.Generator) -> np.ndarray:
|
||||||
|
i = int(np.clip(np.searchsorted(self.EDGES, d, side="right") - 1,
|
||||||
|
0, self.EDGES.size - 2))
|
||||||
|
pool = self._pools[i]
|
||||||
|
if pool.size < 32: # на этой дальности возвратов нет
|
||||||
|
pool = self._all
|
||||||
|
if pool.size == 0:
|
||||||
|
return np.full(n, 5.0, np.float32)
|
||||||
|
return rng.choice(pool, size=n).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def local_intensity(prev: np.ndarray, pool: np.ndarray, frame: np.ndarray,
|
||||||
|
rng: np.random.Generator, env: "IntensityEnv | None" = None,
|
||||||
|
d: float = 0.0) -> np.ndarray:
|
||||||
|
"""Яркость вставки по окружению. Отражения среди аргументов нет намеренно.
|
||||||
|
|
||||||
|
Дальность участвует ровно в одном качестве — какую полосу реальных
|
||||||
|
возвратов брать для лучей, ушедших в пустоту. Никакого закона яркости от
|
||||||
|
дальности здесь не задаётся.
|
||||||
|
"""
|
||||||
|
v = np.asarray(prev, np.float32).copy()
|
||||||
|
miss = ~(v > 0)
|
||||||
|
if miss.any():
|
||||||
|
n = int(miss.sum())
|
||||||
|
if env is not None:
|
||||||
|
v[miss] = env.sample(d, n, rng)
|
||||||
|
else:
|
||||||
|
src = pool[pool > 0] if pool.size else np.empty(0, np.float32)
|
||||||
|
if src.size == 0: # все лучи предмета в пустоту
|
||||||
|
wide = frame[::997]
|
||||||
|
src = wide[wide > 0]
|
||||||
|
v[miss] = (rng.choice(src, size=n) if src.size else np.float32(5.0))
|
||||||
|
v = v * rng.lognormal(0.0, INTEN_SPREAD, v.shape)
|
||||||
|
return np.clip(v, 1.0, 255.0).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
def channel_max_range() -> np.ndarray:
|
def channel_max_range() -> np.ndarray:
|
||||||
"""Паспортная дальность каждого канала при 10 % отражения, (128,).
|
"""Паспортная дальность каждого канала при 10 % отражения, (128,).
|
||||||
|
|
@ -235,7 +332,8 @@ def dropout_probability(r: np.ndarray, reflectivity: float,
|
||||||
def inject(pc: PointCloud2, layout: ScanLayout, plane: RailPlane,
|
def inject(pc: PointCloud2, layout: ScanLayout, plane: RailPlane,
|
||||||
obj: ObjectModel, place: Placement, *,
|
obj: ObjectModel, place: Placement, *,
|
||||||
rng: np.random.Generator | None = None,
|
rng: np.random.Generator | None = None,
|
||||||
range_noise: float = 0.02, cols: slice | None = None) -> tuple[PointCloud2, dict]:
|
range_noise: float = 0.02, cols: slice | None = None,
|
||||||
|
env: "IntensityEnv | None" = None) -> tuple[PointCloud2, dict]:
|
||||||
"""Вставить предмет в облако точек. Возвращает (новое облако, разметка)."""
|
"""Вставить предмет в облако точек. Возвращает (новое облако, разметка)."""
|
||||||
rng = rng or np.random.default_rng()
|
rng = rng or np.random.default_rng()
|
||||||
n_rings, n_az, n_echo = layout.n_rings, layout.n_az, layout.n_echo
|
n_rings, n_az, n_echo = layout.n_rings, layout.n_az, layout.n_echo
|
||||||
|
|
@ -286,6 +384,10 @@ def inject(pc: PointCloud2, layout: ScanLayout, plane: RailPlane,
|
||||||
r_exist = np.minimum(r_exist, r_e)
|
r_exist = np.minimum(r_exist, r_e)
|
||||||
|
|
||||||
closer = new_r < r_exist
|
closer = new_r < r_exist
|
||||||
|
# яркость того, что было на лучах предмета: на выбранных — то, что он
|
||||||
|
# заслонил, остальные идут в запасной набор для лучей без эха
|
||||||
|
prev_sel = pts["intensity"][base[closer]]
|
||||||
|
pool = pts["intensity"][base]
|
||||||
ring_i, col_i, base, new_r = ring_i[closer], col_i[closer], base[closer], new_r[closer]
|
ring_i, col_i, base, new_r = ring_i[closer], col_i[closer], base[closer], new_r[closer]
|
||||||
n_written = int(base.size)
|
n_written = int(base.size)
|
||||||
if n_written:
|
if n_written:
|
||||||
|
|
@ -293,9 +395,8 @@ def inject(pc: PointCloud2, layout: ScanLayout, plane: RailPlane,
|
||||||
nx = (dir_sel[:, 0] * new_r).astype(np.float32)
|
nx = (dir_sel[:, 0] * new_r).astype(np.float32)
|
||||||
ny = (dir_sel[:, 1] * new_r).astype(np.float32)
|
ny = (dir_sel[:, 1] * new_r).astype(np.float32)
|
||||||
nz = (dir_sel[:, 2] * new_r).astype(np.float32)
|
nz = (dir_sel[:, 2] * new_r).astype(np.float32)
|
||||||
# ламбертова интенсивность: ρ·cosθ/r², приведена к шкале прибора 0…255
|
inten = local_intensity(prev_sel, pool, pts["intensity"], rng,
|
||||||
cos_inc = np.clip(np.abs(dir_sel[:, 1]), 0.05, 1.0)
|
env=env, d=place.d)
|
||||||
inten = np.clip(2.2e4 * obj.reflectivity * cos_inc / (new_r ** 2), 1, 255)
|
|
||||||
for e in range(n_echo):
|
for e in range(n_echo):
|
||||||
idx = base + e * n_rings
|
idx = base + e * n_rings
|
||||||
fx[idx] = nx
|
fx[idx] = nx
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,46 @@ def test_mbon_absent_leaves_pipeline_unchanged():
|
||||||
assert _quality(c, mbon_blend=1.0) == _quality(c, mbon_blend=0.0)
|
assert _quality(c, mbon_blend=1.0) == _quality(c, mbon_blend=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- модель сенсора: яркость
|
||||||
|
|
||||||
|
def test_injected_intensity_copies_the_surroundings():
|
||||||
|
"""Яркость вставки берётся из записи, а не назначается.
|
||||||
|
|
||||||
|
Абсолютной шкалы интенсивности в данных нет: медиана кандидатов по бэгам
|
||||||
|
3…7, а в записи с настоящим предметом 23.5. Любое назначенное число делает
|
||||||
|
вставку опознаваемой по одной яркости — сначала как «тускло = предмет»
|
||||||
|
(ламбертова ρ·cosθ/r²), потом как «ярко = предмет» (постоянные 35).
|
||||||
|
Поэтому у функции нет аргументов ни дальности, ни отражения.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from flyguard.synth import local_intensity
|
||||||
|
|
||||||
|
# отражения среди аргументов нет; дальность есть, но только как выбор
|
||||||
|
# полосы реальных возвратов для лучей, ушедших в пустоту
|
||||||
|
args = set(inspect.signature(local_intensity).parameters)
|
||||||
|
assert not args & {"reflectivity", "rho", "cos_inc"}
|
||||||
|
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
prev = np.array([10.0, 20.0, 0.0, 0.0], np.float32) # два луча в пустоту
|
||||||
|
pool = np.full(50, 30.0, np.float32)
|
||||||
|
frame = np.full(10_000, 7.0, np.float32)
|
||||||
|
v = local_intensity(prev, pool, frame, rng)
|
||||||
|
|
||||||
|
assert 8.0 < v[0] < 13.0 and 16.0 < v[1] < 25.0 # взято с тех же лучей
|
||||||
|
assert 24.0 < v[2] < 38.0 and 24.0 < v[3] < 38.0 # взято из запаса
|
||||||
|
|
||||||
|
# запас пуст — остаётся кадр целиком
|
||||||
|
v2 = local_intensity(np.zeros(200, np.float32), np.zeros(0, np.float32),
|
||||||
|
frame, rng)
|
||||||
|
assert 5.5 < float(np.median(v2)) < 9.0
|
||||||
|
|
||||||
|
# без разложения окружения по дальности сама дальность ничего не меняет
|
||||||
|
a = local_intensity(prev, pool, frame, np.random.default_rng(3), d=20.0)
|
||||||
|
b = local_intensity(prev, pool, frame, np.random.default_rng(3), d=200.0)
|
||||||
|
assert np.allclose(a, b)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- данные
|
# --------------------------------------------------------------------------- данные
|
||||||
|
|
||||||
@pytest.mark.skipif(not DATA.exists(), reason="датасет не распакован")
|
@pytest.mark.skipif(not DATA.exists(), reason="датасет не распакован")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Печатает, в скольких кадрах он попал в кандидаты и в подтверждённые треки,
|
Печатает, в скольких кадрах он попал в кандидаты и в подтверждённые треки,
|
||||||
и сколько при этом было посторонних тревог.
|
и сколько при этом было посторонних тревог.
|
||||||
|
|
||||||
python tools/check_obstacle.py [--memory artifacts/mushroom_body.npz]
|
python tools/check_obstacle.py [--memory artifacts/mushroom_body.npz] [--mbon artifacts/mbon_folds/mbon_roundT_doubleT.npz]
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -23,12 +23,20 @@ def main() -> None:
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("--bag", default=str(B.DATA / "for_hackathon" / "doubleT_obstacle"))
|
ap.add_argument("--bag", default=str(B.DATA / "for_hackathon" / "doubleT_obstacle"))
|
||||||
ap.add_argument("--memory")
|
ap.add_argument("--memory")
|
||||||
|
ap.add_argument("--mbon", default="",
|
||||||
|
help="обученное считывание; для честной цифры берите складку, "
|
||||||
|
"не видевшую этот бэг: artifacts/mbon_folds/mbon_roundT_doubleT.npz")
|
||||||
ap.add_argument("--limit", type=int, default=200)
|
ap.add_argument("--limit", type=int, default=200)
|
||||||
ap.add_argument("--verbose", action="store_true")
|
ap.add_argument("--verbose", action="store_true")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
memory = MushroomBody.load(args.memory) if args.memory else None
|
memory = MushroomBody.load(args.memory) if args.memory else None
|
||||||
fg = FlyGuard(Params(), memory=memory)
|
readout = None
|
||||||
|
if args.mbon:
|
||||||
|
from flyguard.mbon_readout import MbonReadout
|
||||||
|
readout = MbonReadout.load(args.mbon)
|
||||||
|
print(f"считывание MBON: {args.mbon}")
|
||||||
|
fg = FlyGuard(Params(), memory=memory, readout=readout)
|
||||||
bag = Bag(args.bag)
|
bag = Bag(args.bag)
|
||||||
|
|
||||||
n = cand_hit = track_hit = other = 0
|
n = cand_hit = track_hit = other = 0
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,12 @@ def main() -> None:
|
||||||
help="1 — только модель, 0 — только ручная формула")
|
help="1 — только модель, 0 — только ручная формула")
|
||||||
ap.add_argument("--mbon-power", type=float, default=None,
|
ap.add_argument("--mbon-power", type=float, default=None,
|
||||||
help="резкость вероятности модели")
|
help="резкость вероятности модели")
|
||||||
|
ap.add_argument("--warn", type=float, default=None,
|
||||||
|
help="порог улики для тревоги (по умолчанию 0.5)")
|
||||||
|
ap.add_argument("--clear", type=float, default=None,
|
||||||
|
help="нижний порог гистерезиса (по умолчанию 0.3)")
|
||||||
|
ap.add_argument("--min-hits", type=int, default=None,
|
||||||
|
help="наблюдений, без которых трек не считается")
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
|
@ -135,6 +141,13 @@ def main() -> None:
|
||||||
over["mbon_blend"] = args.mbon_blend
|
over["mbon_blend"] = args.mbon_blend
|
||||||
if args.mbon_power is not None:
|
if args.mbon_power is not None:
|
||||||
over["mbon_power"] = args.mbon_power
|
over["mbon_power"] = args.mbon_power
|
||||||
|
if args.warn is not None:
|
||||||
|
over["warn_evidence"] = args.warn
|
||||||
|
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
||||||
|
elif args.clear is not None:
|
||||||
|
over["clear_evidence"] = args.clear
|
||||||
|
if args.min_hits is not None:
|
||||||
|
over["min_hits"] = args.min_hits
|
||||||
params = Params(**over)
|
params = Params(**over)
|
||||||
readout = None
|
readout = None
|
||||||
folds = {}
|
folds = {}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import _bootstrap as B # noqa: F401
|
||||||
from flyguard.bag import Bag, find_bags
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.mushroom_body import MushroomBody
|
from flyguard.mushroom_body import MushroomBody
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
from flyguard.synth import Placement, catalogue, inject
|
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
||||||
|
|
||||||
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
||||||
|
|
||||||
|
|
@ -67,6 +67,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
if gres is None or s_track[k] is None:
|
if gres is None or s_track[k] is None:
|
||||||
continue
|
continue
|
||||||
s_now = s_track[k] - have[0]
|
s_now = s_track[k] - have[0]
|
||||||
|
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||||||
for i, (name, lat) in enumerate(scen):
|
for i, (name, lat) in enumerate(scen):
|
||||||
d_true = d_start - s_now
|
d_true = d_start - s_now
|
||||||
if d_true < 6.0:
|
if d_true < 6.0:
|
||||||
|
|
@ -76,7 +77,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
# сенсора, он окажется в стене, а не в габарите.
|
# сенсора, он окажется в стене, а не в габарите.
|
||||||
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
||||||
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[name],
|
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[name],
|
||||||
Placement(d=d_true, u=u_obj), rng=rng)
|
Placement(d=d_true, u=u_obj), rng=rng, env=env)
|
||||||
res = pipes[i].process(pc2)
|
res = pipes[i].process(pc2)
|
||||||
if res is None:
|
if res is None:
|
||||||
continue
|
continue
|
||||||
|
|
@ -102,24 +103,52 @@ def main() -> None:
|
||||||
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
||||||
ap.add_argument("--memory")
|
ap.add_argument("--memory")
|
||||||
ap.add_argument("--mbon", default="",
|
ap.add_argument("--mbon", default="",
|
||||||
help="обученное считывание MBON; как и память, оно "
|
help="обученное считывание MBON одной моделью; она видела "
|
||||||
"видело эти бэги: полигон меряет дальность, "
|
"эти бэги — годится только для отладки")
|
||||||
"а не обобщаемость")
|
ap.add_argument("--mbon-dir", default="",
|
||||||
|
help="каталог с моделями по складкам (mbon_<бэг>.npz): для "
|
||||||
|
"каждого бэга берётся модель, его НЕ видевшая. Иначе "
|
||||||
|
"дальность завышена: считывание обучалось ровно на "
|
||||||
|
"таких же вставках в этот же тоннель")
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "benchmark.json"))
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "benchmark.json"))
|
||||||
ap.add_argument("--limit", type=int, default=250)
|
ap.add_argument("--limit", type=int, default=250)
|
||||||
ap.add_argument("--d-start", type=float, default=200.0)
|
ap.add_argument("--d-start", type=float, default=200.0)
|
||||||
ap.add_argument("--laterals", default="0.0,0.9")
|
ap.add_argument("--laterals", default="0.0,0.9")
|
||||||
ap.add_argument("--seed", type=int, default=12345)
|
ap.add_argument("--seed", type=int, default=12345)
|
||||||
|
ap.add_argument("--mbon-blend", type=float, default=None,
|
||||||
|
help="1 — только модель, 0 — только ручная формула")
|
||||||
|
ap.add_argument("--warn", type=float, default=None,
|
||||||
|
help="порог улики для тревоги (по умолчанию 0.5)")
|
||||||
|
ap.add_argument("--clear", type=float, default=None)
|
||||||
|
ap.add_argument("--min-hits", type=int, default=None)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||||
memory = MushroomBody.load(args.memory) if args.memory else None
|
memory = MushroomBody.load(args.memory) if args.memory else None
|
||||||
readout = None
|
readout = None
|
||||||
if args.mbon:
|
folds: dict = {}
|
||||||
|
if args.mbon_dir:
|
||||||
|
from pathlib import Path as _P
|
||||||
|
from flyguard.mbon_readout import MbonReadout
|
||||||
|
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
|
||||||
|
folds[f.stem[len("mbon_"):]] = MbonReadout.load(f)
|
||||||
|
print(f"считывание MBON по складкам: {args.mbon_dir} "
|
||||||
|
f"({len(folds)} моделей)")
|
||||||
|
elif args.mbon:
|
||||||
from flyguard.mbon_readout import MbonReadout
|
from flyguard.mbon_readout import MbonReadout
|
||||||
readout = MbonReadout.load(args.mbon)
|
readout = MbonReadout.load(args.mbon)
|
||||||
print(f"считывание MBON: {args.mbon}")
|
print(f"считывание MBON: {args.mbon}")
|
||||||
params = Params()
|
over = {}
|
||||||
|
if args.mbon_blend is not None:
|
||||||
|
over["mbon_blend"] = args.mbon_blend
|
||||||
|
if args.warn is not None:
|
||||||
|
over["warn_evidence"] = args.warn
|
||||||
|
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
||||||
|
elif args.clear is not None:
|
||||||
|
over["clear_evidence"] = args.clear
|
||||||
|
if args.min_hits is not None:
|
||||||
|
over["min_hits"] = args.min_hits
|
||||||
|
params = Params(**over)
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
|
|
||||||
all_rec = []
|
all_rec = []
|
||||||
|
|
@ -127,8 +156,14 @@ def main() -> None:
|
||||||
if p.name == HOLDOUT:
|
if p.name == HOLDOUT:
|
||||||
continue
|
continue
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
|
rd = readout
|
||||||
|
if folds:
|
||||||
|
rd = folds.get(p.name)
|
||||||
|
if rd is None:
|
||||||
|
print(f" внимание: для {p.name} нет своей складки — пропуск")
|
||||||
|
continue
|
||||||
rec = run_bag(p, params, memory, args.limit, args.d_start, laterals,
|
rec = run_bag(p, params, memory, args.limit, args.d_start, laterals,
|
||||||
args.seed, readout=readout)
|
args.seed, readout=rd)
|
||||||
all_rec.extend(rec)
|
all_rec.extend(rec)
|
||||||
n = sum(len(r["d"]) for r in rec)
|
n = sum(len(r["d"]) for r in rec)
|
||||||
print(f" {p.name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}, "
|
print(f" {p.name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}, "
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import _bootstrap as B # noqa: F401
|
||||||
from flyguard.bag import Bag, find_bags
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.mushroom_body import FEATURES, describe
|
from flyguard.mushroom_body import FEATURES, describe
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
from flyguard.synth import Placement, catalogue, inject
|
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
||||||
|
|
||||||
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
|
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
|
||||||
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
|
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
|
||||||
|
|
@ -77,13 +77,14 @@ def collect_bag(path, params: Params, limit: int, d_starts, laterals, seed: int)
|
||||||
if g is None or s_track[k] is None:
|
if g is None or s_track[k] is None:
|
||||||
continue
|
continue
|
||||||
s_now = s_track[k] - s0
|
s_now = s_track[k] - s0
|
||||||
|
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||||||
for i, (name, lat, d0) in enumerate(scen):
|
for i, (name, lat, d0) in enumerate(scen):
|
||||||
d_true = d0 - s_now
|
d_true = d0 - s_now
|
||||||
if d_true < 6.0:
|
if d_true < 6.0:
|
||||||
continue
|
continue
|
||||||
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
||||||
pc2, lab = inject(pc, layout, g.plane, cat[name],
|
pc2, lab = inject(pc, layout, g.plane, cat[name],
|
||||||
Placement(d=d_true, u=u), rng=rng)
|
Placement(d=d_true, u=u), rng=rng, env=env)
|
||||||
res = pipes[i].process(pc2)
|
res = pipes[i].process(pc2)
|
||||||
if res is None or not res.candidates:
|
if res is None or not res.candidates:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use("Agg")
|
matplotlib.use("Agg")
|
||||||
import matplotlib.pyplot as plt # noqa: E402
|
import matplotlib.pyplot as plt # noqa: E402
|
||||||
|
|
||||||
import numpy as np # noqa: E402
|
import numpy as np # noqa: E402
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401,E402
|
import _bootstrap as B # noqa: F401,E402
|
||||||
|
|
@ -61,6 +63,7 @@ def main() -> None:
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("--in", dest="path", default=str(B.ARTIFACTS / "benchmark.json"))
|
ap.add_argument("--in", dest="path", default=str(B.ARTIFACTS / "benchmark.json"))
|
||||||
ap.add_argument("--thr", type=float, default=0.5)
|
ap.add_argument("--thr", type=float, default=0.5)
|
||||||
|
ap.add_argument("--out", default="", help="куда сохранить рисунок")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
rec = json.load(open(args.path, encoding="utf-8"))
|
rec = json.load(open(args.path, encoding="utf-8"))
|
||||||
|
|
@ -112,7 +115,7 @@ def main() -> None:
|
||||||
ax.legend(fontsize=7, ncol=2)
|
ax.legend(fontsize=7, ncol=2)
|
||||||
|
|
||||||
B.FIGURES.mkdir(parents=True, exist_ok=True)
|
B.FIGURES.mkdir(parents=True, exist_ok=True)
|
||||||
out = B.FIGURES / "detection_range.png"
|
out = Path(args.out) if args.out else B.FIGURES / "detection_range.png"
|
||||||
fig.tight_layout(); fig.savefig(out)
|
fig.tight_layout(); fig.savefig(out)
|
||||||
print("\nсохранено:", out)
|
print("\nсохранено:", out)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,24 @@ BANDS = ((0, 30), (30, 55), (55, 80), (80, 110), (110, 160), (160, 230))
|
||||||
|
|
||||||
|
|
||||||
def auc(score: np.ndarray, y: np.ndarray) -> float:
|
def auc(score: np.ndarray, y: np.ndarray) -> float:
|
||||||
pos, neg = score[y == 1], score[y == 0]
|
"""Ранговый AUC со СРЕДНИМИ рангами на совпадениях.
|
||||||
if pos.size == 0 or neg.size == 0:
|
|
||||||
|
Без усреднения признак, у которого все значения равны (за 160 м таков,
|
||||||
|
например, контраст к фону — там его структурно нет), получает AUC 0 или 1
|
||||||
|
просто по порядку в массиве. Один такой ложный сигнал уже стоил разбора.
|
||||||
|
"""
|
||||||
|
n_pos, n_neg = int((y == 1).sum()), int((y == 0).sum())
|
||||||
|
if n_pos == 0 or n_neg == 0:
|
||||||
return float("nan")
|
return float("nan")
|
||||||
order = np.argsort(np.concatenate([pos, neg]))
|
order = np.argsort(score, kind="mergesort")
|
||||||
ranks = np.empty(order.size, np.float64)
|
s = score[order]
|
||||||
ranks[order] = np.arange(1, order.size + 1)
|
start = np.flatnonzero(np.r_[True, s[1:] != s[:-1]])
|
||||||
return float((ranks[:pos.size].sum() - pos.size * (pos.size + 1) / 2)
|
end = np.r_[start[1:], s.size]
|
||||||
/ (pos.size * neg.size))
|
avg = (start + end - 1) / 2.0 + 1.0
|
||||||
|
ranks = np.empty(s.size, np.float64)
|
||||||
|
ranks[order] = np.repeat(avg, end - start)
|
||||||
|
return float((ranks[y == 1].sum() - n_pos * (n_pos + 1) / 2)
|
||||||
|
/ (n_pos * n_neg))
|
||||||
|
|
||||||
|
|
||||||
def fpr_at_tpr(score: np.ndarray, y: np.ndarray, tpr: float = 0.95) -> float:
|
def fpr_at_tpr(score: np.ndarray, y: np.ndarray, tpr: float = 0.95) -> float:
|
||||||
|
|
@ -106,6 +116,35 @@ def main() -> None:
|
||||||
print(f" {n:<42}{X[n].shape[0]:7d} предметов {int(Y[n].sum()):6d} "
|
print(f" {n:<42}{X[n].shape[0]:7d} предметов {int(Y[n].sum()):6d} "
|
||||||
f"({Y[n].mean():5.1%})")
|
f"({Y[n].mean():5.1%})")
|
||||||
|
|
||||||
|
# --------------------------------------------- что можно выучить по одному
|
||||||
|
# признаку. Эта таблица нужна не для качества, а для ловли артефактов
|
||||||
|
# вставки: если один признак сам по себе почти идеально делит предмет и
|
||||||
|
# обстановку — особенно с обратным знаком на дальности, — значит модель
|
||||||
|
# сенсора врёт, а не детектор хорош. Так была найдена интенсивность
|
||||||
|
# (EXPERIMENTS п. 11.1).
|
||||||
|
Xa = np.concatenate([X[n] for n in names])
|
||||||
|
ya = np.concatenate([Y[n] for n in names])
|
||||||
|
Da = np.concatenate([D[n] for n in names])
|
||||||
|
single = np.array([auc(Xa[:, j], ya) for j in range(n_pn)])
|
||||||
|
print()
|
||||||
|
print("что делит выборку само по себе (AUC одного признака):")
|
||||||
|
for j in np.argsort(-np.abs(single - 0.5))[:8]:
|
||||||
|
print(f" {feats[j]:<14}{single[j]:.3f}")
|
||||||
|
worst = []
|
||||||
|
for j in range(n_pn):
|
||||||
|
for lo, hi in BANDS:
|
||||||
|
m = (Da >= lo) & (Da < hi)
|
||||||
|
if (ya[m] == 1).sum() < 20:
|
||||||
|
continue
|
||||||
|
a = auc(Xa[m, j], ya[m])
|
||||||
|
if a > 0.97 or a < 0.03:
|
||||||
|
worst.append((feats[j], lo, hi, a))
|
||||||
|
if worst:
|
||||||
|
print(" ВНИМАНИЕ: один признак почти полностью решает задачу — "
|
||||||
|
"похоже на артефакт вставки, а не на сигнал:")
|
||||||
|
for f, lo, hi, a in worst[:8]:
|
||||||
|
print(f" {f:<14}{lo:4d}-{hi:<4d} м AUC {a:.3f}")
|
||||||
|
|
||||||
# ------------------------------------------------------ развёртка по ёмкости
|
# ------------------------------------------------------ развёртка по ёмкости
|
||||||
best_kc = args.n_kc
|
best_kc = args.n_kc
|
||||||
if args.sweep_kc.strip():
|
if args.sweep_kc.strip():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue