bone_2026/assets/labeling.md

324 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Разметка датасета и выбор правила метки
Документ описывает, откуда берутся метки снимков, как проверялось правило
разметки и какие результаты из этого следуют. Числа проверяемы: ссылки на
источники приведены рядом.
## 1. Задача разметки
Экспертная оценка в наборе сделана на уровне **исследования**: в таблице
`разметка.xlsx` по каждому исследованию отмечены критерии качества, а не по
отдельному снимку. Модель же работает со снимками, поэтому вердикт исследования
нужно было перенести на каждый снимок — без догадок и без потери информации о
том, откуда метка взялась.
## 2. Экспертная таблица
Столбцы (две строки заголовков, данные с третьей):
| Столбец | Смысл | Тип нарушения |
|---|---|---|
| 2 | Позвоночник: укладка | `positioning` |
| 3 | Позвоночник: ось | `axis_deviation` |
| 4 | Позвоночник: артефакты, наложения | `artifact` |
| 5, 6 | Бедро R: позиционирование/ротация, область интереса | `rotation`, `roi_incorrect` |
| 7, 8 | Бедро L: то же | `rotation`, `roi_incorrect` |
| 9, 10, 11 | Итог по области | — |
| 12 | Комментарий эксперта | — |
**Семантика значений** установлена по данным, а не по формулировкам заголовков:
- `1` в столбце критерия означает **нарушение**, хотя часть заголовков
сформулирована положительно («корректная укладка»);
- итог области равен логическому ИЛИ критериев: бёдра 72/72 и 78/78,
позвоночник 96/99;
- три расхождения позвоночника (два случая «итог без критериев», один «критерий
без итога») трактуются как нарушение — по правилу «хотя бы один существенный
пункт нарушен»;
- заполненность: позвоночник 99/100, бедро R 72/100, бедро L 78/100; у 23
исследований есть комментарий.
## 3. Перенос вердикта на снимок
| Шаг | Что делается | Почему так |
|---|---|---|
| Ключ склейки | имя каталога исследования | таблица ссылается на каталог (`2.25…`), а в DICOM лежит другой идентификатор (`1.2.643…`); соответствие каталог → тег 100/100 |
| Склейка дублей | по хешу пиксельных данных | 478 файлов — это 251 уникальный снимок |
| Область снимка | голосование по именам файлов группы | один снимок назван и как позвоночник, и как бедро; при равенстве голосов область остаётся неопределённой (такой снимок один) |
| Вердикт | оценка области переносится на её снимок | **каждая область встречается в исследовании ровно один раз**, поэтому не нужно решать, какой из нескольких снимков «плохой» |
| Сторона бедра | при единственном снимке бедра берётся единственный заполненный столбец | в 71 из 72 исследований с двумя бёдрами столбцы совпадают с именами файлов; в 7 исследованиях с одним снимком заполнена противоположная сторона. Факт переноса фиксируется флагом `laterality_mirrored`; теги `Laterality` в DICOM пусты |
| Тип нарушения | только из структурированных критериев | комментарии («сколиоз», «эндопротезирование ТБС») сохранены дословно: перекладывать свободный текст в код — догадка |
## 4. Правило метки и почему оно такое
Метка могла строиться двумя способами: только из таблицы или с добавлением
пометок, которые вручную проставлялись в именах файлов (суффикс `_bad`). Пометки
в именах оказались ненадёжными: они расходились с оценкой эксперта в **62 случаях
из 251**. Выбор сделан измерением, а не по вкусу.
**Постановка.** Разбиение по исследованиям зафиксировано один раз, оба варианта
обучены пятью seed'ами на нём, оценены по одному эталону — вердикту эксперта на
снимках валидации. Снимки валидации не участвуют в обучении ни в одном варианте.
Воспроизведение: `python -m src.dxa.excel_labels --label-rule {table,union,expert}`
и `python -m src.dxa.compare_labels --split-file labels/split_expert_seed42.json`.
**Результат** (53 снимка валидации, 16 нарушений по эталону, 5 seed'ов):
| Метрика (эталон) | только таблица | таблица или суффикс `_bad` |
|---|---|---|
| ROC-AUC | **0.6726** [0.6367, 0.7086] | 0.6003 [0.5592, 0.6415] |
| PR-AUC | **0.4753** [0.4188, 0.5318] | 0.3864 [0.3487, 0.4242] |
| F1 | **0.5559** [0.5212, 0.5906] | 0.5354 [0.4978, 0.5729] |
| Recall | 0.7125 | 0.7875 |
| Precision | 0.4716 | 0.4087 |
Парная разница («только таблица» − «с суффиксами»):
| Метрика | Δ, среднее [95 % ДИ] | Знаки по seed'ам |
|---|---|---|
| ROC-AUC | **+0.0723** [+0.0541, +0.0905] | `+++++` |
| PR-AUC | **+0.0889** [+0.0500, +0.1277] | `+++++` |
| F1 | +0.0205 [−0.0182, +0.0592] | `+-+-+` |
| Precision | +0.0629 [−0.0133, +0.1392] | `+++-+` |
| Recall | −0.0750 [−0.1931, +0.0431] | `0---0` |
**Решение: принято правило «только экспертная таблица».** Дополнительные пометки
из имён файлов ухудшали согласие модели с экспертом на невиданных
исследованиях: вариант с ними чаще срабатывал (recall 0.79), но за счёт
точности, а по беспороговым метрикам проигрывал на всех пяти seed'ах.
Оговорка: эталон — та же экспертная таблица, поэтому вариант, обучавшийся
непосредственно на ней, находится в выигрышном положении. Значимо здесь другое:
добавление ненадёжных пометок **снижает** согласие с экспертом, что и служит
аргументом против них.
## 5. Словарь типов нарушений
| Код | Подпись | Область | Источник |
|---|---|---|---|
| `positioning` | Некорректная укладка | позвоночник | таблица |
| `axis_deviation` | Отклонение оси | позвоночник | таблица |
| `artifact` | Артефакты и импланты | любая | таблица |
| `rotation` | Ротация, позиционирование | бедро | таблица |
| `roi_incorrect` | Некорректная область интереса | любая | таблица |
| `motion` | Движение, размытие | любая | критерии методики |
| `incomplete_anatomy` | Анатомия видна не полностью | любая | критерии методики |
| `labeling_error` | Ошибка разметки | позвоночник | критерии методики |
| `unspecified` | Нарушение без уточнения | любая | служебный |
Распределение в разметке: `rotation` 35, `artifact` 17, `axis_deviation` 10,
`roi_incorrect` 7, `positioning` 6, `unspecified` 5. Всего 80 критериев на 76
нарушений: у части снимков таблица отмечает по два критерия.
Словарь один на всё решение (`src/dxa/violations.py`): коды используют инференс,
отчёт DICOM SR и веб-интерфейс, подписи отдаёт сервер, копий в JavaScript нет.
## 6. Результат разметки
`labels/labels_images.csv` (и XLSX) — по одной строке на уникальный снимок:
| | позвоночник | бедро R | бедро L | неопред. | всего |
|---|---|---|---|---|---|
| качественных | 66 | 58 | 51 | 0 | 175 |
| с нарушением | 33 | 20 | 22 | 1 | 76 |
| **итого** | **99** | **78** | **73** | **1** | **251** |
Помимо метки в CSV сохранено происхождение: `quality_from_excel` (вердикт
эксперта), `quality_from_filename` (пометка из имени файла), `sources_conflict`,
`label_rule`, `filename_fallback`, `laterality_mirrored`, `region_ambiguous`,
`expert_comment`. Поэтому правило можно переиграть без повторного разбора.
Рядом лежат варианты для воспроизведения сравнения:
`labels_images_table.csv`, `labels_images_union.csv`, `labels_images_expert.csv`
(эталон: только снимки с экспертной оценкой, 248 строк) и
`split_expert_seed42.json` (зафиксированное разбиение).
## 7. Гигиена данных: имена файлов
Имена проставлялись вручную и разошлись: `spine_1` и `spine_01`, `Spine_01`,
`r_spine_03`, `r_hip03`, `r_hop_02` (опечатка), `spine-1`. Они приведены к виду
`<область>_<NN>[_good|_bad].dcm` инструментом `src/dxa/rename_files.py`;
переименовано 344 файла из 548, карта отката — `labels/rename_map.csv`.
Переименование сделано **после** того, как разметка и метрики были посчитаны, и
проверено, что оно на них не влияет: набор из 252 пиксельных групп идентичен до и
после, разметка не изменилась ни в одной строке. Суффиксы `_good`/`_bad`
сохранены как были и в метках не участвуют — только как диагностический столбец.
Числа в этих двух абзацах — состояние на момент проверки; актуальное состояние
набора и разметки приведено ниже.
### Расхождение разметки с датасетом и его устранение (2026-09-27)
К описанному проходу добавился более поздний: 2026-09-27 около 02:02 суффиксы
`_good`/`_bad` дописаны ещё 54 файлам, то есть **после** сборки разметки
(2026-09-26 22:35). `apply_excel_labels` сопоставляет метки строго по
`path_to_image`, поэтому во всех четырёх файлах разметки (`labels_images.csv`,
`_table`, `_union`, `_expert`) 20 строк стали ссылаться на имена, которых больше
нет. Последствие: эти 20 снимков теряли экспертную метку и падали на пометку из
имени файла, у 6 из них метка инвертировалась 0 → 1 (вердикт эксперта — «годное»,
файл назван `..._bad.dcm`), и пайплайн отдавал 251 снимок / 82 нарушения вместо
записанных выше 252 / 77. Расхождение нашли сравнением меток с именами файлов;
тесты падали на числах (`assert 251 == 252`), то есть оно было замечено, а не
осталось незамеченным.
Устранено пересборкой: `./run.sh label` и три варианта правила, `./run.sh split`,
затем переобучение `./run.sh train`. Актуальное состояние — 251 снимок / 76
нарушений, все пути разметки существуют, расхождений с датасетом нет. Пометки в
именах при этом расходятся с вердиктом эксперта в **62 случаях из 251** (было 15
из 252): после второго переименования пометок стало больше, и они чаще спорят с
таблицей. На метку это не влияет — правило `table` их не использует (см. §4), —
но именно эти 62 снимка имеет смысл разобрать первыми в интерфейсе ручной
разметки (см. §10).
Чистка датасета в тот же день: удалены 38 лишних байт-идентичных копий `.dcm`
внутри `НД_для_обучения` и 10 файлов `.DS_Store`. На разметку и на набор
пиксельных групп это не влияет: групп 251, все совпадают с `path_to_image`. На
диске 482 `.dcm` из 494 файлов. У каждого удалённого файла остался байт-идентичный
близнец, поэтому потеряно только имя, а не содержимое.
## 8. Оценка качества модели
Разбиение по исследованиям (по умолчанию seed 42): обучение 198 снимков /
81 исследование, валидация 53 снимка / 19 исследований, 16 нарушений.
| Метрика | Значение | Как получено |
|---|---|---|
| ROC-AUC | **0.6726** [0.6367, 0.7086] | 5 seed'ов на фиксированном разбиении, эталон — вердикт эксперта |
| PR-AUC | 0.4753 [0.4188, 0.5318] | там же |
| F1 | 0.5559 [0.5212, 0.5906] | там же; порог подобран по F1 на валидации, поэтому смещён вверх |
| ROC-AUC по областям | позвоночник 0.929, бедро R 0.606, бедро L 0.567 | рабочий чекпоинт, его собственная валидация |
Рабочий чекпоинт — обычный прогон с seed по умолчанию (эпоха 57, порог логита
−0.1370 → вероятность 0.466), его собственная валидационная ROC-AUC 0.6689
близка к среднему по seed'ам, то есть результат не отобран по удачности.
Проверка, что модель смотрит на снимок, а не угадывает анатомию: внутри областей
она даёт AUC 0.88–0.95, правило «позвоночник значит нарушение» — ровно 0.50.
Числа считаются на всём наборе, включая обучающие снимки, поэтому смещены вверх и
отвечают на вопрос «есть ли вклад содержимого», а не «каково качество на новых
данных».
## 9. Ограничения
1. **Разметка унаследована от исследования.** Таблица оценивает исследование, а
не снимок; перенос однозначен, потому что область встречается один раз, но
исходная оценка всё равно не поштучная.
2. **Мало данных:** 251 снимок, 76 нарушений. Интервалы широкие.
3. **Эталон — та же таблица.** Независимой истины нет; вариант, обучавшийся на
таблице, в сравнении в выигрышном положении.
4. **Тип нарушения — эвристика**, а не вывод модели: 5 снимков имеют только
`unspecified`, у остальных тип приходит из критериев таблицы.
5. **Три снимка без экспертной оценки** размечены по пометке в имени файла и
помечены `filename_fallback`.
6. **Сторона бедра в 7 исследованиях не проверяема:** теги латеральности пусты.
7. **Корректность областей интереса наследуется из таблицы:** разметки ROI в
DICOM нет, сравнить её напрямую не с чем.
8. **Пометки в именах файлов спорят с таблицей в 62 случаях из 251.** На метку
это не влияет (правило `table`), но означает, что один из двух источников
ошибается почти в четверти набора. Разбирать их поштучно можно в `/label`; для
выбора правила измерения в §8 такие снимки не использовались.
9. **Поштучной разметки всё ещё нет.** Интерфейс `/label` (§10) её даёт, но им
ещё не пользовались: пока набор размечен на уровне исследования.
## 10. Ручная разметка: интерфейс `/label`
Поштучной экспертной оценки снимков в наборе нет — это ограничение №1, и
автоматически его не закрыть: локальная vision-модель оказалась непригодна (§11),
а экспертная таблица описывает исследование. Поэтому в сервисе есть интерфейс
ручной разметки — раздел 2.6 задания прямо просит «автоматическую коррекцию
разметки с возможностью подтверждения специалистом».
```bash
./run.sh serve # http://localhost:8000/label
```
Что он делает:
* показывает снимок целиком (PNG через `/api/v1/labeling/image`) без наложений,
чтобы разметчик судил по изображению, а не по подсказке алгоритма;
* отдаёт список снимков с текущей меткой и её источником (`table`, `filename` или
`manual`), причём снимки с расхождением метки и пометки в имени файла идут
первыми — именно там один из источников ошибается (см. §9, п. 8);
* сохраняет вердикт: анатомическая область, «годное / нарушение», тип нарушения из
`violations.py` и комментарий. Тип нарушения проверяется на совместимость с
областью (ротация — критерий бедра), у качественного снимка типа быть не может,
а опечатка в коде не превращается молча в `unspecified`;
* пишет вердикты в `labels/manual_labels.csv`. Формат тот же, что у построенной
разметки, поэтому файл читается `load_labels_csv` и принимается обучением как
`--labels-csv`;
* выгружает `scope=all` — весь набор с наложенными ручными вердиктами (готовый
источник меток для `./run.sh train`) — или `scope=reviewed` — только разобранные
снимки, чтобы сверить их с построенной разметкой.
Оценка модели в интерфейсе намеренно не показывается: разметчик, видя подсказку,
соглашался бы с ней, и поштучная разметка теряла бы ценность независимого
суждения. Прогресс по областям и число расхождений показаны, чтобы работу можно
было вести частями и прерывать.
Горячие клавиши: `g` — годное, `b` — нарушение, `Enter` — сохранить и перейти к
следующему, `d` — снять вердикт, `j`/`k` — навигация по списку.
Инструмент не заменяет эксперта: он только фиксирует суждение специалиста в
формате, который пайплайн уже умеет читать.
### Передача врачу: автономный пакет
`/label` требует запущенного сервиса и Python, а размечать должен врач — обычно на
своей машине, вне сети и без установки чего-либо. Поэтому тот же сценарий
собирается в один HTML-файл со встроенными снимками (`src/dxa/review_pack.py`):
```bash
python -m src.dxa.review_pack --out review/doctor_review.html # 251 снимок, ~7 МБ
python -m src.dxa.review_pack --merge review/doctor.csv \
--out labels/labels_images_reviewed.csv # наложить вердикты врача
```
Страница не делает ни одного сетевого запроса (проверяется тестом), вердикты
хранит браузер, выгрузка — CSV в формате `manual_labels.csv` с дополнительной
колонкой `pack_id` (отпечаток набора: по нему видно, что файл вернулся из того же
пакета). Модель в пакет не попадает: если показать разметчику оценку алгоритма,
он будет с ней соглашаться, и независимого суждения не получится.
**Что стоит размечать.** Порядок в пакете и в интерфейсе — как в §7: сперва 62
снимка, где метка разметки расходится с пометкой в имени файла (там ошибается
один из двух источников). Но одних расхождений мало: они отобраны по признаку,
который сам может быть смещён. Чтобы получить **независимую** оценку качества,
нужен ещё случайный подсчёт — если врач разметит, например, случайные 50 снимков
из 251, по ним можно будет измерить ROC-AUC и F1 модели на человеческом суждении,
а не на той же экспертной таблице, из которой выведена разметка (ограничение 3 в
§9). Такой замер в проекте ещё не делался: ни один снимок пока не размечен
поштучно.
## 11. Отрицательный результат: локальная vision-модель
Планировалась визуальная разметка локальной vision-моделью (9 млрд параметров,
офлайн), чтобы не зависеть от таблицы. На калибровке по 14 снимкам, из которых 8
заведомо с нарушениями, модель вынесла «непригоден» всем 14, включая все
качественные, с шаблонными формулировками и выдуманными имплантами. Разделяющая
способность — на уровне случайной, поэтому как разметчик модель непригодна.
Вывод, который стоит зафиксировать: разделяющую способность инструмента нужно
проверять **до** того, как строить на нём пайплайн. Инструменты рендера снимков и
контактных листов остались в `src/dxa/render.py` — они полезны для выборочной
ручной проверки.
## 12. Воспроизведение
```bash
./run.sh label # разметка по экспертной таблице
./run.sh rename # имена файлов (план; --apply)
./run.sh split && ./run.sh compare # выбор правила метки, 5 seed'ов
python -m src.dxa.excel_labels --label-rule union --out-name labels_images_union
python -m src.dxa.render --by-region --out dataset_hack/_preview
```
## 13. Что дальше
- Поштучная разметка снимков специалистом через `/label` (§10) — снимет
ограничение №1 и позволит перемерить качество на независимом суждении.
- Мультилейбл по типам нарушений вместо эвристики: в интерфейсе тип уже
проставляется вручную, но модель его не предсказывает.
- Больше исследований (500+), чтобы сузить интервалы.
- Калибровка порога определения области под конкретное оборудование.