999 lines
93 KiB
HTML
999 lines
93 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Техническое описание — автоматизированная оценка качества DXA</title>
|
||
<style>
|
||
@page { size: A4; margin: 18mm 16mm 16mm 16mm; }
|
||
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||
body {
|
||
font: 10.5pt/1.5 "PT Serif", Georgia, "Times New Roman", serif;
|
||
color: #14181d; margin: 0; max-width: none;
|
||
}
|
||
h1, h2, h3, h4 { font-family: "Helvetica Neue", Arial, sans-serif; color: #0f1c2e; line-height: 1.25; }
|
||
h1 { font-size: 19pt; margin: 0 0 6pt; }
|
||
h2 {
|
||
font-size: 13pt; margin: 20pt 0 8pt; padding-bottom: 3pt;
|
||
border-bottom: 1.2pt solid #1f4e79; page-break-after: avoid;
|
||
}
|
||
h3 { font-size: 11pt; margin: 14pt 0 5pt; page-break-after: avoid; }
|
||
h4 { font-size: 10.5pt; margin: 11pt 0 4pt; page-break-after: avoid; }
|
||
p { margin: 0 0 7pt; text-align: justify; }
|
||
ul, ol { margin: 0 0 7pt; padding-left: 16pt; }
|
||
li { margin-bottom: 2.5pt; }
|
||
code, .mono { font-family: "SFMono-Regular", Menlo, Consolas, monospace; font-size: 9pt; background: #f4f6f8; padding: 0 2pt; }
|
||
pre {
|
||
font-family: "SFMono-Regular", Menlo, Consolas, monospace; font-size: 8.8pt;
|
||
background: #f6f8fa; border: 0.6pt solid #d6dde4; border-radius: 3pt;
|
||
padding: 6pt 8pt; margin: 0 0 8pt; white-space: pre-wrap; page-break-inside: avoid;
|
||
}
|
||
table { border-collapse: collapse; width: 100%; margin: 0 0 9pt; font-size: 9.5pt; page-break-inside: avoid; }
|
||
th, td { border: 0.6pt solid #b9c4ce; padding: 3.5pt 5pt; text-align: left; vertical-align: top; }
|
||
th { background: #eaeff4; font-family: "Helvetica Neue", Arial, sans-serif; font-size: 9pt; }
|
||
td.num, th.num { text-align: right; white-space: nowrap; }
|
||
.cover { border: 1pt solid #1f4e79; border-left: 6pt solid #1f4e79; padding: 12pt 14pt; margin-bottom: 14pt; }
|
||
.cover .sub { font-size: 11.5pt; color: #35506e; margin: 2pt 0 10pt; }
|
||
.cover table { margin-bottom: 0; font-size: 9.5pt; }
|
||
.cover th { width: 34%; background: #f3f6f9; }
|
||
.lead { font-size: 10.5pt; background: #f3f6f9; border-left: 3pt solid #6b8fb4; padding: 7pt 9pt; margin-bottom: 12pt; }
|
||
.note { font-size: 9.5pt; background: #fdf6e3; border: 0.6pt solid #e0c893; border-radius: 3pt; padding: 6pt 8pt; margin: 0 0 9pt; }
|
||
.flag { font-family: "Helvetica Neue", Arial, sans-serif; font-size: 8.5pt; font-weight: 600; color: #8a5a00; background: #fdf0d5; border: 0.6pt solid #e0c893; border-radius: 2pt; padding: 0 3pt; white-space: nowrap; }
|
||
.toc { page-break-after: always; }
|
||
.toc ol { list-style: none; padding-left: 0; counter-reset: toc; }
|
||
.toc li { margin-bottom: 3pt; font-size: 10pt; }
|
||
.toc a { color: #1f4e79; text-decoration: none; }
|
||
.flow { margin: 0 0 9pt; page-break-inside: avoid; }
|
||
.flow table { font-size: 9pt; }
|
||
.flow td, .flow th { text-align: center; vertical-align: middle; }
|
||
.flow .arrow { border: 0; width: 14pt; font-size: 12pt; color: #1f4e79; }
|
||
.page-break { page-break-before: always; }
|
||
.imgs { display: block; page-break-inside: avoid; margin-bottom: 10pt; }
|
||
.imgs img { width: 100%; border: 0.6pt solid #b9c4ce; border-radius: 3pt; }
|
||
.imgs.two { display: flex; gap: 6pt; }
|
||
.imgs .cap { font-size: 8.8pt; color: #4a5a6a; margin-top: 3pt; }
|
||
figure { margin: 0 0 10pt; page-break-inside: avoid; }
|
||
figure img { width: 100%; border: 0.6pt solid #b9c4ce; border-radius: 3pt; }
|
||
figcaption { font-size: 8.8pt; color: #4a5a6a; margin-top: 3pt; }
|
||
.sign { margin-top: 16pt; font-size: 9.5pt; color: #35506e; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="cover">
|
||
<h1>Автоматизированная оценка качества денситометрических исследований</h1>
|
||
<div class="sub">Техническое описание решения</div>
|
||
<table>
|
||
<tr><th>Задача</th><td>Контроль качества DXA-исследований: бинарная оценка «качественное / есть нарушение», определение анатомической области, тип нарушения, структурированный отчёт</td></tr>
|
||
<tr><th>Дата документа</th><td>27 сентября 2026 г.</td></tr>
|
||
<tr><th>Команда</th><td>Грачев Денис — разработка; Грачев Татьяна — капитан</td></tr>
|
||
<tr><th>Версия решения</th><td>1.0.0 (<code>src/__init__.py</code>)</td></tr>
|
||
<tr><th>Рабочий чекпоинт</th><td><code>models/dxa_model.pth</code>: ResNet18 + линейная голова, разметка <code>table</code>, seed 42, эпоха 39, порог логита −0.4930 (вероятность 0.379)</td></tr>
|
||
<tr><th>Ключевые метрики</th><td>ROC-AUC 0.6764 [0.6309, 0.7218], F1 0.5676 [0.5270, 0.6082] — пять seed'ов на фиксированном разбиении по исследованиям</td></tr>
|
||
<tr><th>Время обработки</th><td>0.021 с на изображение (медиана, CPU), 548 файлов за 12.1 с; требование «≤ 3 мин на исследование» выполняется с запасом</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p class="lead">Документ описывает фактическое состояние репозитория на дату, указанную на титуле:
|
||
все параметры, пороги, метрики и имена полей приведены по коду и по выполненным измерениям, а не по
|
||
проектным намерениям. Там, где значение измерено на конкретной машине, указано, на какой.
|
||
Расхождения и ограничения перечислены явно — разделы 15 и 16.</p>
|
||
|
||
<div class="toc">
|
||
<h2 style="margin-top:0">Содержание</h2>
|
||
<ol>
|
||
<li><a href="#s1">1. Назначение и область применения</a></li>
|
||
<li><a href="#s2">2. Соответствие требованиям задания</a></li>
|
||
<li><a href="#s3">3. Архитектура решения</a></li>
|
||
<li><a href="#s4">4. Состав данных и разметка</a></li>
|
||
<li><a href="#s5">5. Предобработка изображений</a></li>
|
||
<li><a href="#s6">6. Модель и процедура обучения</a></li>
|
||
<li><a href="#s7">7. Определение анатомической области</a></li>
|
||
<li><a href="#s8">8. Таксономия нарушений качества</a></li>
|
||
<li><a href="#s9">9. Форматы входных и выходных данных</a></li>
|
||
<li><a href="#s10">10. API сервиса</a></li>
|
||
<li><a href="#s11">11. Веб-интерфейс</a></li>
|
||
<li><a href="#s12">12. Метрики качества</a></li>
|
||
<li><a href="#s13">13. Производительность и системные требования</a></li>
|
||
<li><a href="#s14">14. Сборка, запуск и развёртывание</a></li>
|
||
<li><a href="#s15">15. Тесты и проверки</a></li>
|
||
<li><a href="#s16">16. Известные ошибки и их обработка</a></li>
|
||
<li><a href="#s17">17. Ограничения и достоверность результатов</a></li>
|
||
<li><a href="#s18">18. План развития</a></li>
|
||
<li><a href="#a1">Приложение А. Структура репозитория</a></li>
|
||
<li><a href="#a2">Приложение Б. Команды</a></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h2 id="s1">1. Назначение и область применения</h2>
|
||
|
||
<p>Сервис принимает денситометрическое исследование в формате DICOM и выполняет автоматизированный
|
||
контроль его качества: определяет анатомическую область, решает бинарную задачу «изображение пригодно
|
||
для клинической интерпретации / содержит нарушение качества», относит нарушение к категории и
|
||
формирует структурированный отчёт (XLSX/CSV) с одной строкой на изображение.</p>
|
||
|
||
<p>Задача решается как цифровой помощник, а не как замена врача: результат предназначен для отбора
|
||
исследований, требующих внимания специалиста, и для единообразной регистрации нарушений. Области
|
||
анализа — поясничный отдел позвоночника и проксимальный отдел бедренной кости (левая и правая
|
||
стороны рассматриваются как отдельные снимки, поскольку сторона влияет на трактовку критериев).</p>
|
||
|
||
<p>Решение работает полностью офлайн: изображения не передаются во внешние сервисы, все зависимости и
|
||
веса фиксированы, доступ в сеть при инференсе не требуется. Это прямое требование методики
|
||
(п. 3.2 задания) и одновременно условие применимости в закрытом контуре медицинской организации.</p>
|
||
|
||
<h2 id="s2">2. Соответствие требованиям задания</h2>
|
||
|
||
<p>Таблица связывает пункты задания с реализацией и со способом проверки. Формулировки требований
|
||
приведены по тексту методических рекомендаций.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:34%">Требование</th><th style="width:40%">Реализация</th><th>Чем подтверждается</th></tr>
|
||
<tr><td>Области: поясничный отдел позвоночника и проксимальный отдел бедренной кости</td>
|
||
<td>Область определяется автоматически по кадру; бедро разделяется на левое и правое</td>
|
||
<td>Разд. 7; на валидации 99/99 для позвоночника</td></tr>
|
||
<tr><td>Бинарная классификация «качественное / есть нарушение»</td>
|
||
<td>ResNet18 (ImageNet) + линейная голова, порог по логиту, подобранный по F1</td>
|
||
<td>Разд. 6, 12; <code>tests/test_preprocess_and_model.py</code></td></tr>
|
||
<tr><td>Определение типа нарушения</td>
|
||
<td>Единый словарь кодов <code>src/dxa/violations.py</code>; отнесение к категории — по измеряемым признакам снимка</td>
|
||
<td>Разд. 8; <code>tests/test_violations.py</code></td></tr>
|
||
<tr><td>Оценка корректности разметки анатомических структур</td>
|
||
<td>В наборе нет ROI-разметки в DICOM, поэтому оценивается геометрия видимой зоны измерения и её границы</td>
|
||
<td>Разд. 8, 17 (ограничение 4)</td></tr>
|
||
<tr><td>Отчёт <code>.xlsx</code>/<code>.csv</code>, одна строка на изображение, столбцы из п. 2.5</td>
|
||
<td>CLI-инференс и <code>POST /api/v1/export</code>; обязательные восемь столбцов плюс три диагностических</td>
|
||
<td>Разд. 9 (состав столбцов проверен на реальном прогоне)</td></tr>
|
||
<tr><td>Время обработки одного исследования ≤ 3 мин</td>
|
||
<td>Один снимок — один прямой проход сети 224×224, без итеративных процедур</td>
|
||
<td>Разд. 13: 0.021 с медиана на CPU, запас более чем трёхкратный</td></tr>
|
||
<tr><td>Отсутствие необработанных исключений; ошибки фиксируются в отчёте</td>
|
||
<td>Ошибка на файле не прерывает пакет: строка получает <code>processing_status = Failure: …</code></td>
|
||
<td>Разд. 16</td></tr>
|
||
<tr><td>Воспроизводимость при повторном запуске</td>
|
||
<td>Фиксированный seed, зафиксированное разбиение в файле, версии зависимостей, самодостаточный чекпоинт</td>
|
||
<td>Разд. 6, 14; <code>tests/test_labels.py</code></td></tr>
|
||
<tr><td>Пакетная обработка архива + общая таблица + zip с дополнительными сериями</td>
|
||
<td>Обход каталога, лог прогресса, XLSX/CSV, опциональный zip с визуализацией</td>
|
||
<td>Разд. 9, 13</td></tr>
|
||
<tr><td>API для пакетной обработки тестового набора</td>
|
||
<td>FastAPI: восемь маршрутов, включая <code>/api/v1/batch</code> и <code>/api/v1/export</code></td>
|
||
<td>Разд. 10</td></tr>
|
||
<tr><td>Обязательная контейнеризация, скрипт сборки и запуска в Linux</td>
|
||
<td><code>Dockerfile</code> (python:3.11-slim), <code>Dockerfile_cuda</code> для GPU, <code>run.sh</code></td>
|
||
<td>Разд. 14; сборка проверена, см. 14</td></tr>
|
||
<tr><td>Фиксация зависимостей, включая базовый контейнер</td>
|
||
<td><code>requirements.txt</code> с точными версиями, базовый образ по тегу, torch с CPU-индексом</td>
|
||
<td>Разд. 14</td></tr>
|
||
<tr><td>Работа без обращения к внешним сервисам</td>
|
||
<td>Офлайн-ассеты фронтенда, отсутствие сетевых вызовов в коде предсказания</td>
|
||
<td><code>tests/browser/ui_offline.js</code></td></tr>
|
||
<tr><td>Полный комплект документации</td>
|
||
<td><code>README.md</code> (обзор и воспроизведение), <code>assets/labeling.md</code> (разметка и её обоснование), <code>docs/technical-description.html</code> и <code>.pdf</code> (данный документ)</td>
|
||
<td>—</td></tr>
|
||
</table>
|
||
|
||
<h2 id="s3">3. Архитектура решения</h2>
|
||
|
||
<h3>3.1. Слои</h3>
|
||
|
||
<table>
|
||
<tr><th style="width:22%">Слой</th><th style="width:26%">Файлы</th><th>Ответственность</th></tr>
|
||
<tr><td>Прикладной ядерный слой</td><td><code>src/dxa/</code></td>
|
||
<td>Чтение DICOM, предобработка, модель, метрики, определение области, разметка данных, обучение, пакетный инференс</td></tr>
|
||
<tr><td>Сервис</td><td><code>src/main.py</code></td>
|
||
<td>FastAPI: загрузка чекпоинта при старте, маршруты анализа, пакетной обработки, экспорта, здоровья и карточки решения</td></tr>
|
||
<tr><td>Веб-интерфейс</td><td><code>src/api/static/</code></td>
|
||
<td>Загрузка файлов, таблица результатов, панель деталей, панель «О модели»; все подписи и метрики получает с сервера</td></tr>
|
||
<tr><td>Эвристики качества</td><td><code>src/quality/</code></td>
|
||
<td>Числовые измерения снимка (резкость, плотные включения, полнота и поворот области) для панели деталей</td></tr>
|
||
<tr><td>Инфраструктура</td><td><code>run.sh</code>, <code>Dockerfile</code>, <code>Dockerfile_cuda</code>, <code>Jenkinsfile</code></td>
|
||
<td>Единая точка входа для сборки, обучения, инференса и тестов; сборка образа и выкладка</td></tr>
|
||
</table>
|
||
|
||
<h3>3.2. Поток обработки одного изображения</h3>
|
||
|
||
<p>Ниже — фактический путь данных от файла до строки отчёта. Ключевое свойство: обучение и сервис
|
||
используют <em>один и тот же</em> код предобработки и предсказания, поэтому расхождение между
|
||
офлайн-оценкой и работой API невозможно по построению, а не по договорённости.</p>
|
||
|
||
<div class="flow">
|
||
<table>
|
||
<tr>
|
||
<th>DICOM</th><td class="arrow">→</td>
|
||
<th>Чтение пикселей и метаданных</th><td class="arrow">→</td>
|
||
<th>Предобработка 224×224</th><td class="arrow">→</td>
|
||
<th>ResNet18 + линейная голова</th>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="7" style="border:0; height:4pt"></td>
|
||
</tr>
|
||
<tr>
|
||
<th>Логит → порог</th><td class="arrow">→</td>
|
||
<th>Область по кадру</th><td class="arrow">→</td>
|
||
<th>Числовые измерения</th><td class="arrow">→</td>
|
||
<th>Строка отчёта / ответ API</th>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<p>Порядок операций и точка принятия решения:</p>
|
||
<ol>
|
||
<li><b>Чтение.</b> Из файла берутся пиксельные данные и идентификаторы исследования и снимка
|
||
(<code>StudyInstanceUID</code>, <code>SOPInstanceUID</code>). Персональные данные не используются:
|
||
решение не зависит от их наличия.</li>
|
||
<li><b>Предобработка.</b> Приводится к виду, ожидаемому предобученным backbone (описание — разд. 5).
|
||
Параметры предобработки хранятся в чекпоинте и берутся из него, а не из кода по умолчанию.</li>
|
||
<li><b>Оценка качества.</b> Один прямой проход сети даёт логит. Решение принимается по логиту:
|
||
<code>quality_class = 1</code>, если логит выше порога. Порог подобран по F1 на валидации и
|
||
сохранён в чекпоинте (текущее значение −0.4930, что соответствует вероятности 0.379).</li>
|
||
<li><b>Анатомическая область.</b> Определяется независимо от модели, по геометрии кадра (разд. 7),
|
||
с оценкой уверенности.</li>
|
||
<li><b>Измерения и тип нарушения.</b> Для снимков с нарушением вычисляются числовые признаки снимка,
|
||
по которым нарушение относится к категории словаря (разд. 8).</li>
|
||
<li><b>Отчёт.</b> Строка собирается со статусом обработки и временем; ошибка на файле не прерывает пакет.</li>
|
||
</ol>
|
||
|
||
<div class="note">В <code>README.md</code> приведён полный набор проектных диаграмм (компоненты, потоки данных,
|
||
последовательность, развёртывание). Они описывают целевую архитектуру, включая блоки, которые в текущей
|
||
реализации не используются, поэтому в настоящем документе поток описан текстом по коду.</div>
|
||
|
||
<h2 id="s4">4. Состав данных и разметка</h2>
|
||
|
||
<h3>4.1. Набор</h3>
|
||
|
||
<table>
|
||
<tr><th>Показатель</th><th class="num">Значение</th><th>Пояснение</th></tr>
|
||
<tr><td>Файлов на диске</td><td class="num">544</td><td>в обучающем наборе (<code>dataset_hack/НД_для_обучения</code>)</td></tr>
|
||
<tr><td>Уникальных снимков</td><td class="num">252</td><td>по пиксельному содержимому; 292 файла — побайтные дубли</td></tr>
|
||
<tr><td>Исследований</td><td class="num">100</td><td>разбиение выполняется по исследованиям, а не по снимкам</td></tr>
|
||
<tr><td>Позвоночник / бедро правое / бедро левое / не определено</td><td class="num">99 / 79 / 73 / 1</td><td>голосование по именам файлов после склейки дублей</td></tr>
|
||
<tr><td>Нарушений по экспертной таблице</td><td class="num">74 (29.4 %)</td><td>три снимка таблица не оценивала</td></tr>
|
||
<tr><td>Нарушений в рабочей разметке</td><td class="num">77 (30.6 %)</td><td>74 по таблице плюс 3 снимка с пометкой в имени файла</td></tr>
|
||
</table>
|
||
|
||
<p>Дубли не пересекают границы исследований, конфликтов меток при склейке не возникает. Два побайтных
|
||
дубля названы по-разному, поэтому область определяется голосованием по именам файлов. Изучение
|
||
набора выявило две особенности, которые пришлось учесть в разметке: один и тот же снимок встречается
|
||
под несколькими именами (без склейки он попадал бы одновременно в оба класса), и имена файлов местами
|
||
расходятся с оценкой эксперта.</p>
|
||
|
||
<h3>4.2. Единица разметки и правило получения метки</h3>
|
||
|
||
<p>Экспертная таблица описывает <b>исследование</b>, а не отдельный снимок. Однако каждая
|
||
анатомическая область встречается в исследовании ровно один раз (после склейки дублей), поэтому
|
||
вердикт исследования по области переносится на снимок однозначно — не требуется решать, какой из
|
||
нескольких снимков «плохой». Так получены метки и типы нарушений (<code>labels/labels_images.csv</code>);
|
||
каждый источник свидетельства сохранён в отдельном столбце, поэтому правило можно переиграть без
|
||
повторного разбора данных.</p>
|
||
|
||
<p>Возможны были два правила: учитывать только экспертную таблицу либо дополнительно учитывать
|
||
пометки <code>_good</code>/<code>_bad</code>, проставленные вручную в именах файлов. Пометки
|
||
расходились с экспертом в 15 случаях из 252, поэтому правило выбиралось измерением: одно
|
||
зафиксированное разбиение, пять seed'ов обучения, один эталон (табл. в разд. 12.3). Выбрано правило
|
||
«только экспертная таблица».</p>
|
||
|
||
<h3>4.3. Разбиение выборки</h3>
|
||
|
||
<table>
|
||
<tr><th>Часть</th><th class="num">Снимков</th><th class="num">Исследований</th><th class="num">Нарушений</th><th>Файл</th></tr>
|
||
<tr><td>Обучение</td><td class="num">199</td><td class="num">81</td><td class="num">61</td><td rowspan="2"><code>labels/split_expert_seed42.json</code></td></tr>
|
||
<tr><td>Валидация</td><td class="num">53</td><td class="num">19</td><td class="num">16</td></tr>
|
||
</table>
|
||
|
||
<p>Разбиение стратифицировано по эталону и <b>фиксировано в файле</b>: все сравниваемые варианты
|
||
обучаются и оцениваются на одном и том же held-out наборе, что делает сравнения корректными.
|
||
Снимки одного исследования не попадают одновременно в обе части — это исключает утечку, при которой
|
||
метрики растут за счёт запоминания конкретных исследований. Наличие утечки и воспроизводимость
|
||
разбиения проверяются тестами (<code>tests/test_labels.py</code>).</p>
|
||
|
||
<h2 id="s5">5. Предобработка изображений</h2>
|
||
|
||
<p>Предобработка описана декларативно в <code>src/dxa/preprocess.py</code> (неизменяемая конфигурация
|
||
<code>PreprocessConfig</code>) и используется и при обучении, и при работе API. Параметры хранятся в
|
||
чекпоинте и восстанавливаются из него, поэтому вход модели не может разойтись между двумя режимами.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:24%">Параметр</th><th style="width:16%">Значение</th><th>Назначение</th></tr>
|
||
<tr><td><code>norm</code></td><td><code>percentile</code></td><td>приведение интенсивностей к [0, 1]; допустимо также <code>minmax</code></td></tr>
|
||
<tr><td><code>p_low</code> / <code>p_high</code></td><td>0.5 / 99.5</td><td>отсекаемые перцентили — устойчивость к выбросам и шуму</td></tr>
|
||
<tr><td><code>imagenet_norm</code></td><td><code>true</code></td><td>стандартизация mean (0.485, 0.456, 0.406), std (0.229, 0.224, 0.225), как ожидает предобученный backbone</td></tr>
|
||
<tr><td><code>input_size</code></td><td>224</td><td>сторона квадратного входа сети</td></tr>
|
||
</table>
|
||
|
||
<p>Порядок операций над одним файлом:</p>
|
||
<ol>
|
||
<li><b>Чтение.</b> <code>pydicom.dcmread</code> и <code>pixel_array</code> в <code>float32</code>;
|
||
многокадровое изображение усредняется по кадрам, иная размерность — ошибка.</li>
|
||
<li><b>Коррекция интенсивностей.</b> Учитываются <code>RescaleSlope</code> и
|
||
<code>RescaleIntercept</code>; для <code>MONOCHROME1</code> яркость инвертируется.</li>
|
||
<li><b>Нормировка.</b> По перцентилям 0.5 / 99.5 с обрезкой в [0, 1] и защитой от вырожденного
|
||
диапазона (<code>hi ≤ lo</code>).</li>
|
||
<li><b>Приведение к тензору.</b> Умножение на 255 и <code>uint8</code>, дублирование одного канала
|
||
в три, resize 224×224 билинейно, перевод в CHW, деление на 255, затем нормировка ImageNet.</li>
|
||
</ol>
|
||
|
||
<p>Если в чекпоинте нет блока <code>preprocess</code> (старый файл), применяются значения по умолчанию —
|
||
<code>percentile</code> с нормировкой ImageNet; устаревший профиль <code>minmax</code> без ImageNet
|
||
доступен флагом <code>--no-imagenet-norm</code>.</p>
|
||
|
||
<div class="note">Порядок «resize в <code>uint8</code>, затем нормировка» сохранён намеренно: он совпадает
|
||
с историческим кодом проекта, поэтому смена реализации не сдвинула распределение входа и не обесценила
|
||
ранее подобранный порог.</div>
|
||
|
||
<h2 id="s6">6. Модель и процедура обучения</h2>
|
||
|
||
<h3>6.1. Архитектура</h3>
|
||
|
||
<table>
|
||
<tr><th style="width:26%">Элемент</th><th>Реализация</th></tr>
|
||
<tr><td>Backbone</td>
|
||
<td><code>ResNet18</code> с весами ImageNet (<code>IMAGENET1K_V1</code>), все слои кроме
|
||
<code>fc</code>; признак — 512 чисел. Флагом <code>--backbone</code> допустим также
|
||
<code>ResNet34</code></td></tr>
|
||
<tr><td>Голова качества</td>
|
||
<td>по умолчанию <code>linear</code>: <code>Flatten</code> + <code>Linear(512, 2)</code> — линейный
|
||
зонд, 1026 обучаемых параметров. Режим <code>mlp</code> (флаг <code>--head</code>):
|
||
<code>Linear(512, 256)</code> + <code>ReLU</code> + <code>Dropout(0.3)</code> +
|
||
<code>Linear(256, 2)</code></td></tr>
|
||
<tr><td>Вспомогательная голова области</td>
|
||
<td><code>Flatten</code> + <code>Linear(512, 4)</code>: три анатомические области плюс «неизвестно».
|
||
Обучается с весом 0.3 как дополнительная задача и заставляет backbone различать анатомию; логиты
|
||
качества она не сдвигает</td></tr>
|
||
<tr><td>Стандартизация входа головы</td>
|
||
<td>признаки нормируются по среднему и СКО обучающей выборки, зафиксированным в буферах
|
||
<code>feat_mean</code>/<code>feat_std</code>. Без неё логиты смещены, вероятности скучены у нуля и
|
||
подобранный порог теряет смысл</td></tr>
|
||
</table>
|
||
|
||
<h3>6.2. Гиперпараметры и процедура обучения</h3>
|
||
|
||
<table>
|
||
<tr><th style="width:30%">Параметр</th><th style="width:22%">Значение по умолчанию</th><th>Комментарий</th></tr>
|
||
<tr><td>Оптимизатор</td><td>AdamW</td>
|
||
<td><code>lr</code> 3e-4, <code>weight_decay</code> 5e-2. Заметный weight decay нужен не только
|
||
против переобучения: без него логиты за 100 эпох насыщаются и порог вырождается</td></tr>
|
||
<tr><td>Планировщик</td><td>ReduceLROnPlateau</td><td>множитель 0.5, терпение 3, следит за <code>val_loss</code></td></tr>
|
||
<tr><td>Функция потерь</td><td>CrossEntropy</td>
|
||
<td>балансировка классов выключена (<code>--balance none</code>); доступны <code>loss</code>
|
||
(вес <code>n_good / n_bad</code>) и <code>sampler</code></td></tr>
|
||
<tr><td>Батч / эпохи</td><td>16 / 100</td><td>ранняя остановка после 25 эпох без улучшения</td></tr>
|
||
<tr><td>Отбор чекпоинта</td><td>окно 5</td>
|
||
<td>оценка эпохи — ROC-AUC, сглаженный по последним 5 эпохам; до заполнения окна чекпоинт не
|
||
сохраняется, при равном AUC решает F1</td></tr>
|
||
<tr><td>Вес головы области</td><td>0.3</td><td><code>--region-loss-weight</code>; снимки с неизвестной областью в её loss не входят</td></tr>
|
||
<tr><td>Обрезка градиентов</td><td>норма 5.0</td><td>защита от выбросов</td></tr>
|
||
<tr><td>Заморозка backbone</td><td>всегда</td>
|
||
<td><code>--freeze-epochs -1</code> по умолчанию (линейный зонд); при размораживании lr падает
|
||
в 10 раз</td></tr>
|
||
<tr><td>Seed / устройство</td><td>42 / авто</td><td>порядок выбора устройства: CUDA → MPS → CPU</td></tr>
|
||
</table>
|
||
|
||
<p><b>Почему линейный зонд.</b> При ~250 уникальных снимках полный fine-tune ResNet18 за несколько эпох
|
||
запоминает обучающую выборку: train F1 стремится к 1.0, а val AUC падает к 0.5. Поэтому по умолчанию
|
||
backbone заморожен и обучается только голова на признаках ImageNet. При заморозке слои BatchNorm
|
||
остаются в режиме <code>eval</code>: иначе бегущие статистики продолжали бы обновляться и признаки
|
||
«уезжали» бы от тех, на которых рассчитывалась стандартизация.</p>
|
||
|
||
<p><b>Почему порог по логиту.</b> Порог хранится и применяется в логитах (log-odds): после обучения
|
||
линейный зонд разделяет обучающую выборку почти идеально, вероятности насыщаются в 0/1, и порог в
|
||
единицах вероятности вырождается. Для человека порог переводится в вероятность через сигмоиду. ROC-AUC
|
||
и PR-AUC считаются по вероятностям и от порога не зависят.</p>
|
||
|
||
<p><b>Как подбирается порог.</b> На каждой эпохе по валидации: кандидаты — все наблюдаемые логиты плюс
|
||
края диапазона, выбирается максимум F1; при равном F1 берётся меньший логит, чтобы не терять recall.
|
||
Аргумент <code>--min-recall</code> позволяет вместо этого взять максимальный порог с recall не ниже
|
||
заданного.</p>
|
||
|
||
<p><b>Рабочий чекпоинт.</b> Лучшая эпоха — 39 из прогона в 64 эпохи (обучение остановлено по терпению),
|
||
порог логита −0.4930 (вероятность 0.379). Метрики этой эпохи — в разделе 12, честная оценка варианта на
|
||
пяти seed'ах — ROC-AUC 0.6764.</p>
|
||
|
||
<h3>6.3. Чекпоинт и отчёты</h3>
|
||
|
||
<p>Чекпоинт самодостаточен: кроме весов в нём лежат архитектура, параметры предобработки, порог и
|
||
происхождение данных, поэтому инференс не может рассинхронизироваться с обучением.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:36%">Поле</th><th>Содержимое</th></tr>
|
||
<tr><td><code>model_state_dict</code></td><td>веса сети; <code>format_version</code> — 2</td></tr>
|
||
<tr><td><code>backbone</code>, <code>head</code></td><td>архитектура; при явном расхождении с запрошенной загрузка завершается ошибкой</td></tr>
|
||
<tr><td><code>preprocess</code></td><td>параметры предобработки (раздел 5)</td></tr>
|
||
<tr><td><code>threshold</code>, <code>threshold_logit</code></td><td>рабочий порог по логитам</td></tr>
|
||
<tr><td><code>labels_csv</code>, <code>split_file</code></td><td>на какой разметке и на каком разбиении обучена модель</td></tr>
|
||
<tr><td><code>epoch</code>, <code>val_metrics</code>, <code>selection_score</code></td><td>эпоха, её метрики и сглаженная оценка, по которой выбран чекпоинт</td></tr>
|
||
<tr><td><code>history</code>, <code>optimizer_state_dict</code></td><td>история обучения и состояние оптимизатора</td></tr>
|
||
</table>
|
||
|
||
<p>Рядом с чекпоинтом обучение пишет <code>train_report.md</code> и <code>train_report.json</code>:
|
||
гиперпараметры, метрики на валидации, метрики по областям и историю по эпохам. Артефакты кладутся в
|
||
<code>--output-dir</code> (по умолчанию <code>models</code>).</p>
|
||
|
||
<h2 id="s7">7. Определение анатомической области</h2>
|
||
|
||
<p>Область определяется по содержимому снимка, а не по имени файла: на закрытом наборе соглашение об
|
||
именах может отсутствовать. Решение принимается по геометрии кадра (<code>resolve_region</code> в
|
||
<code>src/dxa/inference.py</code>) независимо от модели качества; обученная вспомогательная голова
|
||
области привлекается только как уточнение.</p>
|
||
|
||
<p>Признаки считает функция <code>heuristic_signals</code> по маске яркой (костной) ткани с порогом по
|
||
95-му перцентилю:</p>
|
||
|
||
<table>
|
||
<tr><th style="width:26%">Признак</th><th>Что измеряет</th></tr>
|
||
<tr><td><code>width</code></td><td>ширина кадра в пикселях — основной признак области на этом оборудовании (у позвоночника 300 px, у бедра 280 px)</td></tr>
|
||
<tr><td><code>bbox_aspect</code></td><td>отношение высоты яркой области к её ширине (вытянутость)</td></tr>
|
||
<tr><td><code>left_right_ratio</code></td><td>перевес светимости левой половины над правой — наклон в сторону бедра</td></tr>
|
||
<tr><td><code>symmetry</code></td><td>симметрия изображения относительно вертикальной оси</td></tr>
|
||
</table>
|
||
|
||
<p>Порог ширины задан константой <code>SPINE_MIN_WIDTH = 295</code>. Порядок решений:</p>
|
||
<ol>
|
||
<li>если ширина кадра ≥ 295 — <code>spine</code>, уверенность 0.8;</li>
|
||
<li>иначе (бедро) — сторона по перевесу светимости: <code>left_right_ratio</code> > 1.3 даёт
|
||
<code>hip_right</code>, < 0.7 — <code>hip_left</code>, уверенность 0.6;</li>
|
||
<li>при промежуточном перевесе — предсказание головы области, если её уверенность ≥ 0.5;</li>
|
||
<li>если и оно неуверенно — общая область <code>hip</code> с уверенностью 0.4.</li>
|
||
</ol>
|
||
|
||
<p>Если ширина кадра неизвестна, используется предсказание головы, а затем форма яркой области
|
||
(<code>bbox_aspect</code> и <code>symmetry</code>).</p>
|
||
|
||
<p><b>Точность.</b> Сопоставление с областью из рабочей разметки (252 снимка, раздел 4):</p>
|
||
|
||
<table>
|
||
<tr><th>Что проверялось</th><th class="num">Результат</th></tr>
|
||
<tr><td>Позвоночник</td><td class="num">99 / 99</td></tr>
|
||
<tr><td>Сторона бедра (правое / левое)</td><td class="num">135 / 152 (88.8 %)</td></tr>
|
||
<tr><td>Итого по всем областям</td><td class="num">234 / 251 (93.2 %)</td></tr>
|
||
</table>
|
||
|
||
<p>Различение позвоночника и бедра по ширине кадра работает безошибочно, а сторона бедра определяется
|
||
менее надёжно: перевес светимости путает левое и правое в 17 случаях из 152. Сторона не проверялась по
|
||
тегам DICOM — <code>Laterality</code> в наборе пуст, — поэтому это ограничение (раздел 17, п. 7), а не
|
||
измеренная ошибка модели.</p>
|
||
|
||
<div class="note">Признак «ширина кадра» привязан к текущему аппарату. Он безошибочен на этом наборе, но
|
||
при смене оборудования порог потребует калибровки — раздел 17, ограничение 8.</div>
|
||
|
||
<h2 id="s8">8. Таксономия нарушений качества</h2>
|
||
|
||
<p>Коды типов нарушений собраны в одном модуле <code>src/dxa/violations.py</code>. Раньше словарь был
|
||
свой в трёх местах (инференс, <code>main.py</code> и веб-интерфейс), и ни одна копия не знала кодов
|
||
экспертной таблицы; теперь коды, русские подписи и пояснения заданы один раз, а сервер отдаёт их
|
||
клиенту.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:22%">Код</th><th style="width:26%">Подпись</th><th style="width:12%">Область</th><th>Источник</th></tr>
|
||
<tr><td><code>positioning</code></td><td>Некорректная укладка</td><td>spine</td><td>экспертная таблица</td></tr>
|
||
<tr><td><code>axis_deviation</code></td><td>Отклонение оси</td><td>spine</td><td>экспертная таблица</td></tr>
|
||
<tr><td><code>artifact</code></td><td>Артефакты и импланты</td><td>любая</td><td>экспертная таблица</td></tr>
|
||
<tr><td><code>rotation</code></td><td>Ротация / позиционирование</td><td>hip</td><td>экспертная таблица</td></tr>
|
||
<tr><td><code>roi_incorrect</code></td><td>Некорректная область интереса</td><td>любая</td><td>экспертная таблица</td></tr>
|
||
<tr><td><code>motion</code></td><td>Движение, размытие</td><td>любая</td><td><code>condition_doctor.txt</code></td></tr>
|
||
<tr><td><code>incomplete_anatomy</code></td><td>Анатомия видна не полностью</td><td>любая</td><td><code>condition_doctor.txt</code></td></tr>
|
||
<tr><td><code>labeling_error</code></td><td>Ошибка разметки</td><td>spine</td><td><code>condition_doctor.txt</code></td></tr>
|
||
<tr><td><code>unspecified</code></td><td>Нарушение без уточнения</td><td>любая</td><td>системный код</td></tr>
|
||
</table>
|
||
|
||
<p>У каждого кода есть пояснение, чем нарушение мешает измерению (например, для <code>rotation</code> —
|
||
что малый вертел виден слишком хорошо и шейка бедра кажется укороченной). Функция
|
||
<code>catalogue()</code> отдаёт словарь через <code>/api/v1/model</code>, поэтому подписи в интерфейсе и
|
||
в выгрузке не могут разойтись с кодами.</p>
|
||
|
||
<p>Для текстового отчёта DICOM SR у каждого кода заданы условный числовой код и английская формулировка.
|
||
Полноценного справочника SNOMED/DICOM для контроля качества DXA в наборе нет, поэтому коды условные и
|
||
всегда идут рядом с текстом: пригодный снимок кодируется <code>113001</code> (DXA image quality
|
||
acceptable) с флагом <code>FINAL</code>, нарушение — с флагом <code>WARNING</code>. Устаревшие значения
|
||
внешних источников (<code>artifact_motion</code>, <code>roi_error</code>, <code>position_error</code>,
|
||
<code>quality_violation_detected</code> и другие) приводит к канону функция <code>canon_type</code>.</p>
|
||
|
||
<h3>8.1. Тип нарушения в разметке</h3>
|
||
|
||
<p>Экспертная таблица кодирует не все девять кодов, а пять: <code>positioning</code>,
|
||
<code>axis_deviation</code>, <code>artifact</code>, <code>rotation</code>, <code>roi_incorrect</code>.
|
||
Распределение в рабочей разметке (в четырёх из 77 нарушений указано по два критерия, поэтому сумма
|
||
больше 77):</p>
|
||
|
||
<table>
|
||
<tr><th>Код</th><th class="num">Снимков</th></tr>
|
||
<tr><td><code>rotation</code></td><td class="num">36</td></tr>
|
||
<tr><td><code>artifact</code></td><td class="num">17</td></tr>
|
||
<tr><td><code>axis_deviation</code></td><td class="num">10</td></tr>
|
||
<tr><td><code>roi_incorrect</code></td><td class="num">7</td></tr>
|
||
<tr><td><code>positioning</code></td><td class="num">6</td></tr>
|
||
<tr><td><code>unspecified</code></td><td class="num">5</td></tr>
|
||
</table>
|
||
|
||
<p>Коды <code>motion</code>, <code>incomplete_anatomy</code> и <code>labeling_error</code> в разметке не
|
||
встречаются: таблица их не кодирует, хотя словарь их знает ради критериев
|
||
<code>condition_doctor.txt</code>. Пять снимков имеют только <code>unspecified</code>, потому что
|
||
источник не указывает критерий.</p>
|
||
|
||
<h3>8.2. Тип нарушения при инференсе</h3>
|
||
|
||
<p>Модель решает только бинарную задачу, поэтому тип для снимка с нарушением выбирает функция
|
||
<code>classify_violation_type</code> по дешёвым признакам: размытие
|
||
(<code>laplacian_variance</code>), доля плотных пикселей (<code>bright_fraction</code>) и вытянутость
|
||
яркой области (<code>bbox_aspect</code> для бедра). При отсутствии выраженного признака возвращается
|
||
<code>unspecified</code>.</p>
|
||
|
||
<div class="note">На текущем чекпоинте эта ветка практически вырождена. Пороги
|
||
<code>motion_threshold</code> и <code>artifact_threshold</code> в вызов не передаются, поэтому
|
||
действуют значения по умолчанию (0.0 и 1.0), которых признаки достичь не могут, а условие ротации
|
||
(<code>bbox_aspect</code> вне диапазона 0.4–3.0) на наборе не срабатывает. Прогон по всем 548 файлам
|
||
даёт одинаковый результат: все 341 решение с нарушением помечены <code>unspecified</code>. Для
|
||
содержательного типа нужна разметка типов на уровне снимка и обученный мультилейбл-классификатор
|
||
(раздел 18), поэтому в ответе API тип всегда идёт с флагом
|
||
<code>violation_type_is_heuristic = true</code>.</div>
|
||
|
||
<h2 id="s9">9. Форматы входных и выходных данных</h2>
|
||
|
||
<h3>9.1. Вход</h3>
|
||
|
||
<p>Один файл DICOM (для API — один HTTP-загрузкой, для CLI — файл или каталог с рекурсивным обходом).
|
||
Исследование может содержать несколько снимков — каждый обрабатывается отдельно и даёт отдельную
|
||
строку отчёта. Перед обработкой файл читается целиком; при ошибке чтения строка получает статус
|
||
сбоя, а пакет продолжается.</p>
|
||
|
||
<h3>9.2. Выходной файл</h3>
|
||
|
||
<p>Состав столбцов проверен на реальных прогонах — и через CLI, и через <code>/api/v1/export</code>.
|
||
Обязательные восемь столбцов задания совпадают в обоих путях по составу и порядку; дополнительно
|
||
пишутся <code>confidence</code> и <code>violation_reason</code>, а CLI-инференс добавляет одиннадцатый
|
||
столбец <code>region_confidence</code>. Таблица ниже отражает CLI-вывод; в ответе
|
||
<code>/api/v1/export</code> последнего столбца нет.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:8%">№</th><th style="width:30%">Столбец</th><th style="width:16%">Тип</th><th>Содержимое</th></tr>
|
||
<tr><td>1</td><td><code>path_to_study</code></td><td>строка</td><td>путь к исследованию (для загрузки через API — <code>upload://имя</code>)</td></tr>
|
||
<tr><td>2</td><td><code>study_uid</code></td><td>строка</td><td><code>StudyInstanceUID</code> из DICOM</td></tr>
|
||
<tr><td>3</td><td><code>image_uid</code></td><td>строка</td><td><code>SOPInstanceUID</code> из DICOM</td></tr>
|
||
<tr><td>4</td><td><code>anatomical_region</code></td><td>строка</td><td><code>spine</code> / <code>hip_left</code> / <code>hip_right</code> / <code>hip</code></td></tr>
|
||
<tr><td>5</td><td><code>quality_class</code></td><td>целое</td><td>0 — качественное, 1 — есть нарушение</td></tr>
|
||
<tr><td>6</td><td><code>violation_type</code></td><td>строка</td><td>код нарушения или пусто</td></tr>
|
||
<tr><td>7</td><td><code>processing_status</code></td><td>строка</td><td><code>Success</code> либо <code>Failure: <причина></code></td></tr>
|
||
<tr><td>8</td><td><code>time_of_processing</code></td><td>вещественное</td><td>время обработки снимка, секунды</td></tr>
|
||
<tr><td>9</td><td><code>confidence</code></td><td>вещественное</td><td>уверенность модели в решении</td></tr>
|
||
<tr><td>10</td><td><code>violation_reason</code></td><td>строка</td><td>текстовое пояснение к отнесённой категории</td></tr>
|
||
<tr><td>11</td><td><code>region_confidence</code></td><td>вещественное</td><td>уверенность определения области</td></tr>
|
||
</table>
|
||
|
||
<div class="note">Время обработки отдаётся в столбце файла результатов; одиночный ответ
|
||
<code>POST /api/v1/analyze</code> этого столбца не содержит — при интеграции время измеряется на
|
||
стороне вызывающей системы (см. разд. 10 и 13).</div>
|
||
|
||
<div class="note">Расхождение ровно в один столбец — единственное различие двух путей выгрузки; оно
|
||
перечислено в разделе 16. Обязательные столбцы и их порядок при этом одинаковы, поэтому автоматический
|
||
разбор результатов не зависит от того, каким путём получен файл.</div>
|
||
|
||
<h2 id="s10">10. API сервиса</h2>
|
||
|
||
<p>Сервис на FastAPI. Маршруты и состав полей ответов ниже приведены по фактическим ответам на
|
||
запросах к тестовым файлам, а не по схеме из документации кода.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:9%">Метод</th><th style="width:27%">Путь</th><th>Назначение</th></tr>
|
||
<tr><td>GET</td><td><code>/</code></td><td>веб-интерфейс</td></tr>
|
||
<tr><td>GET</td><td><code>/api/v1/health</code></td><td>статус сервиса; поля ответа: <code>status</code>, <code>device</code>, <code>model_loaded</code>, <code>model</code></td></tr>
|
||
<tr><td>GET</td><td><code>/api/v1/model</code></td><td>карточка решения: <code>model</code>, <code>labels</code>, <code>dataset</code>, <code>comparison</code>, <code>violations</code>, <code>limitations</code>, <code>sources</code>, <code>snapshot_date</code>, <code>loaded</code></td></tr>
|
||
<tr><td>POST</td><td><code>/api/v1/analyze</code></td><td>анализ одного файла (multipart-загрузка)</td></tr>
|
||
<tr><td>POST</td><td><code>/api/v1/analyze/detailed</code></td><td>расширенный отчёт, опционально с маской</td></tr>
|
||
<tr><td>POST</td><td><code>/api/v1/analyze/sr</code></td><td>текстовое представление отчёта DICOM SR: <code>format</code>, <code>sr_content</code>, UID</td></tr>
|
||
<tr><td>POST</td><td><code>/api/v1/batch</code></td><td>пакетная обработка нескольких файлов</td></tr>
|
||
<tr><td>POST</td><td><code>/api/v1/export</code></td><td>пакетная обработка и выгрузка в XLSX</td></tr>
|
||
</table>
|
||
|
||
<h3>10.1. Состав ответа <code>/api/v1/analyze</code></h3>
|
||
|
||
<p>Поля: <code>filename</code>, <code>study_uid</code>, <code>image_uid</code>,
|
||
<code>anatomical_region</code>, <code>region_confidence</code>, <code>quality_class</code>,
|
||
<code>quality_label</code>, <code>overall_quality</code>, <code>severity</code>,
|
||
<code>confidence</code>, <code>confidence_per_class</code>, <code>threshold_probability</code>,
|
||
<code>violation_type</code>, <code>violation_type_label</code>,
|
||
<code>violation_type_is_heuristic</code>, <code>violation_type_note</code>, <code>reason</code>,
|
||
<code>metrics</code>, <code>samples</code>, <code>processing_status</code>.</p>
|
||
|
||
<p>Два поля заслуживают пояснения при интеграции:</p>
|
||
<ul>
|
||
<li><code>violation_type_is_heuristic = true</code> — тип нарушения определён по измеряемым признакам
|
||
снимка, а не предсказан моделью; модель решает только бинарную задачу. Флаг отдаётся клиенту
|
||
явно, чтобы это различие не терялось в интеграции.</li>
|
||
<li><code>quality_label</code>, <code>violation_type_label</code>, <code>violation_type_note</code> —
|
||
готовые подписи для интерфейса. Они приходят с сервера: клиент не держит собственных копий
|
||
словаря, поэтому подписи не могут разойтись с кодами.</li>
|
||
</ul>
|
||
|
||
<h3>10.2. Расширенный ответ <code>/api/v1/analyze/detailed</code></h3>
|
||
|
||
<p>Дополнительно к предыдущему набору: <code>image</code> (визуализация снимка), <code>mask</code>
|
||
(маска зоны измерения, при запросе), <code>reasons</code>, <code>metrics_note</code>,
|
||
<code>view_quality</code>, а также измерения по областям: <code>spine_completeness</code>,
|
||
<code>hip_completeness</code>, <code>hip_rotation</code>.</p>
|
||
|
||
<div class="note">Измерения в расширенном ответе сопровождаются пометкой о том, что их пороги не
|
||
калиброваны (разд. 17), и не превращаются в вердикты «да/нет»: интерфейс показывает числа.</div>
|
||
|
||
<h2 id="s11">11. Веб-интерфейс</h2>
|
||
|
||
<p>Интерфейс предназначен для ручной проверки работы сервиса и для демонстрации: загрузка файлов,
|
||
таблица результатов со статистикой, панель деталей по выбранной строке и панель «О модели» с
|
||
источником загруженного чекпоинта.</p>
|
||
|
||
<figure>
|
||
<img src="../assets/img/ui-results.png" alt="Таблица результатов: полоса состояния, статистика, строки снимков">
|
||
<figcaption>Рис. 1. Результаты обработки: полоса состояния показывает, какой чекпоинт отвечает
|
||
(разметка, эпоха, порог), рядом — статистика и таблица снимков.</figcaption>
|
||
</figure>
|
||
|
||
<div class="imgs two">
|
||
<figure>
|
||
<img src="../assets/img/ui-detail-violation.png" alt="Панель деталей: снимок с нарушением">
|
||
<figcaption>Рис. 2а. Нарушение: уровень, заключение с вероятностью и порогом, тип нарушения с пометкой «эвристика».</figcaption>
|
||
</figure>
|
||
<figure>
|
||
<img src="../assets/img/ui-detail-clean.png" alt="Панель деталей: качественный снимок">
|
||
<figcaption>Рис. 2б. Качественный снимок: измерения подписаны как справочные.</figcaption>
|
||
</figure>
|
||
</div>
|
||
|
||
<p>Отдельный принцип — <b>интерфейс не утверждает больше, чем известно решению</b>:</p>
|
||
<ul>
|
||
<li>тип нарушения показан с пометкой «эвристика» и пояснением, что модель решает только бинарную задачу;</li>
|
||
<li>метрика рабочего чекпоинта помечена как завышенная (чекпоинт выбирался лучшим из пяти seed'ов),
|
||
а честная оценка варианта приведена рядом — в панели «О модели»;</li>
|
||
<li>плитка средней уверенности не называется точностью: точность требует эталонных меток, которых
|
||
для произвольного файла нет;</li>
|
||
<li>числовые измерения панели деталей идут под дисклеймером о некалиброванности порогов.</li>
|
||
</ul>
|
||
|
||
<figure>
|
||
<img src="../assets/img/ui-model-panel.png" alt="Панель «О модели»: чекпоинт, метрики с интервалами, данные, словарь нарушений">
|
||
<figcaption>Рис. 3. Панель «О модели»: источник чекпоинта, метрики с доверительными интервалами,
|
||
состав данных, словарь нарушений и список ограничений. Данные приходят из <code>/api/v1/model</code>.</figcaption>
|
||
</figure>
|
||
|
||
<h2 id="s12">12. Метрики качества</h2>
|
||
|
||
<h3>12.1. Основная оценка</h3>
|
||
|
||
<p>Оценка получена на фиксированном разбиении по исследованиям (обучение 199 снимков / 81 исследование,
|
||
валидация 53 снимка / 19 исследований, 16 нарушений), пять seed'ов обучения, эталон — вердикт
|
||
эксперта из таблицы. Приоритетные по заданию метрики приведены с 95 % доверительными интервалами.</p>
|
||
|
||
<table>
|
||
<tr><th>Метрика</th><th class="num">Значение</th><th class="num">95 % ДИ</th><th>Комментарий</th></tr>
|
||
<tr><td>ROC-AUC</td><td class="num">0.6764</td><td class="num">[0.6309, 0.7218]</td><td>приоритетная метрика задания</td></tr>
|
||
<tr><td>PR-AUC</td><td class="num">0.4759</td><td class="num">[0.4141, 0.5377]</td><td>базовый уровень при доле нарушений 30 % — около 0.30</td></tr>
|
||
<tr><td>F1</td><td class="num">0.5676</td><td class="num">[0.5270, 0.6082]</td><td>порог подбирался по F1 на той же валидации, поэтому значение смещено вверх</td></tr>
|
||
<tr><td>Recall / Precision</td><td class="num">0.700 / 0.486</td><td class="num">—</td><td>рабочая точка выбранного порога</td></tr>
|
||
</table>
|
||
|
||
<h3>12.2. Оценка по областям и контрольные проверки</h3>
|
||
|
||
<table>
|
||
<tr><th>Что измерено</th><th class="num">Значение</th><th>Как получено</th></tr>
|
||
<tr><td>ROC-AUC: позвоночник / бедро правое / бедро левое</td><td class="num">0.943 / 0.576 / 0.550</td><td>рабочий чекпоинт, собственная валидация</td></tr>
|
||
<tr><td>Контрольная задача «позвоночник / бедро»</td><td class="num">AUC 1.00</td><td>проверка работоспособности пайплайна, а не клиническая метрика</td></tr>
|
||
<tr><td>Модель против правила «позвоночник = нарушение»</td><td class="num">0.854 против 0.529</td><td>оценка на всём наборе, включая обучающие снимки, поэтому смещена вверх</td></tr>
|
||
</table>
|
||
|
||
<p>Разбивка по областям нужна потому, что нарушения распределены неравномерно: в позвоночнике
|
||
33 из 99 снимков, у бёдер 21–22 из 73–79, а сама область почти однозначно определяется по ширине
|
||
кадра. Поэтому общий AUC частично отражает различение области, а не только распознавание дефекта.
|
||
Чтобы отделить одно от другого, выполнена проверка: правило «позвоночник = нарушение» даёт внутри
|
||
областей 0.50 (подсказки нет), модель — 0.85–0.93. Это означает, что модель использует содержимое
|
||
снимка, а не только область.</p>
|
||
|
||
<h3>12.3. Выбор правила разметки</h3>
|
||
|
||
<table>
|
||
<tr><th>Метрика (эталон)</th><th class="num">только таблица</th><th class="num">с суффиксами имён</th></tr>
|
||
<tr><td>ROC-AUC</td><td class="num">0.6764 [0.6309, 0.7218]</td><td class="num">0.6199 [0.5840, 0.6559]</td></tr>
|
||
<tr><td>PR-AUC</td><td class="num">0.4759 [0.4141, 0.5377]</td><td class="num">0.4046 [0.3702, 0.4391]</td></tr>
|
||
<tr><td>F1</td><td class="num">0.5676 [0.5270, 0.6082]</td><td class="num">0.5426 [0.5073, 0.5778]</td></tr>
|
||
<tr><td>Recall / Precision</td><td class="num">0.700 / 0.486</td><td class="num">0.863 / 0.404</td></tr>
|
||
</table>
|
||
|
||
<p>Парная разница (только таблица минус с суффиксами): ROC-AUC <b>+0.0564</b> [+0.0403, +0.0725],
|
||
PR-AUC <b>+0.0713</b> [+0.0398, +0.1028] — знаки <code>+++++</code>, то есть преимущество на всех пяти
|
||
seed'ах. Принято правило «только экспертная таблица». Оговорка, важная для интерпретации: эталон — та
|
||
же таблица, поэтому вариант, обучавшийся на ней, находится в выигрышном положении; значимо здесь то,
|
||
что добавление ненадёжных пометок согласие с экспертом <b>снижает</b>, а не повышает.</p>
|
||
|
||
<h2 id="s13">13. Производительность и системные требования</h2>
|
||
|
||
<h3>13.1. Измерения</h3>
|
||
|
||
<p>Измерения выполнены на рабочей машине разработчика (macOS, Apple Silicon) на полном наборе данных —
|
||
548 файлов в <code>dataset_hack</code> (544 обучающих плюс 4 тестовых). Инференс принудительно
|
||
переведён на CPU параметром <code>--device cpu</code>, чтобы числа не зависели от наличия ускорителя.</p>
|
||
|
||
<table>
|
||
<tr><th>Показатель</th><th class="num">Значение</th><th>Условия</th></tr>
|
||
<tr><td>Время на изображение (медиана)</td><td class="num">0.021 с</td><td>CPU, включает чтение DICOM, предобработку и проход сети</td></tr>
|
||
<tr><td>Время на изображение (максимум)</td><td class="num">0.131 с</td><td>тот же прогон; первый снимок включает прогрев</td></tr>
|
||
<tr><td>Пакет целиком (548 файлов)</td><td class="num">12.1 с</td><td>от запуска процесса до записи XLSX, включая загрузку чекпоинта</td></tr>
|
||
<tr><td>Доля успешно обработанных файлов</td><td class="num">548 / 548 (100 %)</td><td>ошибок чтения на этом наборе нет</td></tr>
|
||
<tr><td>Пиковая память процесса</td><td class="num">≈ 640 МБ</td><td>maximum resident set size, пакетный CPU-инференс</td></tr>
|
||
<tr><td>Ответ API на один файл</td><td class="num">16–18 мс</td><td>вызов внутри процесса, после прогрева; первый запрос 390 мс. Здесь устройство выбрано автоматически (MPS), на CPU значение того же порядка — см. медиану пакетного прогона выше</td></tr>
|
||
<tr><td>Время старта сервиса</td><td class="num">≈ 3 с</td><td>импорт библиотек и загрузка чекпоинта</td></tr>
|
||
</table>
|
||
|
||
<p>Отсюда следует прямой ответ на требование «не более 3 минут на исследование»: даже если
|
||
исследование содержит максимальные по заданию три снимка, обработка занимает доли секунды, а
|
||
трёхминутный бюджет расходуется практически только на передачу файлов и накладные расходы
|
||
интеграции. Запас более чем трёхкратный, поэтому узким местом решение не ограничивает.</p>
|
||
|
||
<h3>13.2. Минимальная и рекомендуемая конфигурация</h3>
|
||
|
||
<table>
|
||
<tr><th style="width:26%">Ресурс</th><th style="width:37%">Минимальная конфигурация</th><th>Рекомендуемая</th></tr>
|
||
<tr><td>Процессор</td><td>2 ядра x86-64 или ARM64</td><td>4 ядра и более; инференс векторизован и масштабируется по кадрам</td></tr>
|
||
<tr><td>Оперативная память</td><td>1.5 ГБ (измеренный пик ≈ 640 МБ + запас на ОС и веб-слой)</td><td>4 ГБ</td></tr>
|
||
<tr><td>Графический ускоритель</td><td><b>Не требуется</b>: torch устанавливается из CPU-индекса, инференс на CPU укладывается в требование по времени</td><td>Любой GPU с поддержкой CUDA 11.8 — вариант сборки <code>Dockerfile_cuda</code></td></tr>
|
||
<tr><td>Дисковое пространство</td><td>базовый образ python:3.11-slim (150 МБ) + зависимости и код; чекпоинт 43 МБ монтируется отдельно</td><td>плюс место под входные архивы DICOM и результаты</td></tr>
|
||
<tr><td>Сеть</td><td>Не требуется при работе: образ и веса фиксированы, обращений во внешние сервисы нет</td><td>Нужна только для сборки образа и загрузки базовых зависимостей</td></tr>
|
||
</table>
|
||
|
||
<p><b>Размер образа (измерено):</b> 1.41 ГБ (1 412 385 143 байт) для сборки под
|
||
<code>linux/arm64</code>. Базовый образ <code>python:3.11-slim</code> занимает 150 МБ, остальное —
|
||
зависимости из <code>requirements.txt</code>, включая torch и torchvision из CPU-индекса; CUDA-колёса
|
||
в образ не попадают. Данные, тесты, <code>labels/</code> и <code>assets/</code> в образ не входят,
|
||
чекпоинт (43 МБ) монтируется отдельно.</p>
|
||
|
||
<h2 id="s14">14. Сборка, запуск и развёртывание</h2>
|
||
|
||
<h3>14.1. Состав образа</h3>
|
||
|
||
<table>
|
||
<tr><th style="width:30%">Элемент</th><th>Значение</th></tr>
|
||
<tr><td>Базовый образ</td><td><code>python:3.11-slim</code> (фиксированный тег)</td></tr>
|
||
<tr><td>Зависимости</td><td><code>requirements.txt</code> с точными версиями; torch и torchvision — из CPU-индекса PyTorch, чтобы в образ не попали CUDA-колёса</td></tr>
|
||
<tr><td>Что копируется в образ</td><td>только <code>src/</code>, <code>requirements.txt</code> и <code>run.sh</code></td></tr>
|
||
<tr><td>Что не копируется</td><td>данные, тесты, <code>labels/</code>, <code>assets/</code>, <code>docs/</code> (см. <code>.dockerignore</code>), а также чекпоинт</td></tr>
|
||
<tr><td>Чекпоинт</td><td>монтируется при запуске в <code>/app/models</code>; путь задаётся переменной <code>DXA_MODEL_PATH</code> (по умолчанию <code>/app/models/dxa_model.pth</code>)</td></tr>
|
||
<tr><td>Проверки на этапе сборки</td><td>импорт приложения (<code>import src.main</code>) и наличие офлайн-ассетов фронтенда; сборка падает при их отсутствии</td></tr>
|
||
<tr><td>Контроль состояния</td><td><code>HEALTHCHECK</code> обращается к <code>/api/v1/health</code> каждые 30 с</td></tr>
|
||
<tr><td>Точка входа</td><td><code>uvicorn src.main:app</code> на порту 8000</td></tr>
|
||
</table>
|
||
|
||
<p>Если чекпоинт не смонтирован, сервис всё равно поднимается: <code>/api/v1/health</code> сообщает
|
||
<code>model_loaded: false</code>, а запросы анализа возвращают ошибку вместо тихой неверной оценки.
|
||
Это сознательный выбор: отсутствие модели должно быть заметно сразу, а не проявляться как «странные»
|
||
результаты.</p>
|
||
|
||
<p><b>Что проверено на самом образе.</b> Сборка прошла обе внутренние проверки
|
||
(<code>app import ok</code>, <code>frontend assets ok</code>). Образ запущен и проверен в двух режимах:
|
||
без смонтированного чекпоинта <code>/api/v1/health</code> отдаёт <code>model_loaded: false</code> и
|
||
<code>exists: false</code>, а запрос анализа — HTTP 500 с телом <code>{"error": "Model not loaded"}</code>;
|
||
с примонтированным каталогом моделей <code>/api/v1/health</code> сообщает <code>model_loaded: true</code>,
|
||
<code>device: cpu</code>, эпоху 39 и файл разметки <code>labels/labels_images.csv</code>, запрос анализа
|
||
отвечает корректной строкой результата, а <code>/api/v1/export</code> возвращает XLSX с ожидаемым
|
||
набором столбцов. Сквозная проверка выполнялась в контейнере на той же машине, где снимались
|
||
измерения производительности.</p>
|
||
|
||
<h3>14.2. Воспроизводимость</h3>
|
||
|
||
<ul>
|
||
<li>Версии всех зависимостей зафиксированы, базовый образ задан тегом, версии torch и torchvision указаны явно.</li>
|
||
<li>Обучение детерминировано по seed; разбиение выборки сохраняется в файл и переиспользуется.</li>
|
||
<li>Чекпоинт самодостаточен: внутри хранятся <code>backbone</code>, <code>head</code>, параметры
|
||
предобработки, <code>threshold</code>, а также <code>labels_csv</code> и <code>split_file</code>.
|
||
Поэтому по файлу видно, на какой разметке и на каком разбиении получена модель, а инференс не
|
||
может рассинхронизироваться с обучением.</li>
|
||
<li>Отказ от зависимости от рабочего каталога: путь к модели и рабочие каталоги задаются переменными окружения.</li>
|
||
</ul>
|
||
|
||
<h3>14.3. Сборочный конвейер</h3>
|
||
|
||
<p><code>Jenkinsfile</code> выполняет три шага: сборка образа, публикация в реестр и выкладка в
|
||
кластер. Скрипт <code>run.sh</code> — единая точка входа для разработки и эксплуатации: обучение,
|
||
разметка, приведение имён файлов, инференс, сервис, сравнение вариантов разметки и тесты
|
||
(перечень команд — приложение Б).</p>
|
||
|
||
<h2 id="s15">15. Тесты и проверки</h2>
|
||
|
||
<p>Тесты — <code>pytest</code>, 209 проверок в шести файлах; запуск — <code>./run.sh test</code> или
|
||
<code>python -m pytest tests/ -q</code>. Сверка выполнена на дату документа: 209 passed.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:30%">Файл</th><th class="num" style="width:10%">Тестов</th><th>Что проверяет</th></tr>
|
||
<tr><td><code>tests/test_labels.py</code></td><td class="num">38</td>
|
||
<td>разбор имён, определение области и метки по имени, стратифицированное разбиение по
|
||
исследованиям, отсутствие утечки, фиксация и переиспользование разбиения в файле, сверка с
|
||
реальным датасетом</td></tr>
|
||
<tr><td><code>tests/test_excel_labels.py</code></td><td class="num">50</td>
|
||
<td>разметка по экспертной таблице: чтение критериев, правило «1 = нарушение», голосование по
|
||
области, перенос оценки на единственное бедро, три правила метки
|
||
(<code>table</code> / <code>union</code> / <code>expert</code>), их согласованность и подключение
|
||
к обучению</td></tr>
|
||
<tr><td><code>tests/test_rename_files.py</code></td><td class="num">36</td>
|
||
<td>приведение имён DICOM: разбор и канонизация, поиск свободного номера при конфликте, отказ
|
||
угадывать область, цикл «применить → откатить»</td></tr>
|
||
<tr><td><code>tests/test_preprocess_and_model.py</code></td><td class="num">32</td>
|
||
<td>конфигурация и нормировка, сборка тензора и нормировка ImageNet, метрики, подбор порога,
|
||
контракт модели, BatchNorm при заморозке, roundtrip чекпоинта, проверка на реальных снимках</td></tr>
|
||
<tr><td><code>tests/test_violations.py</code></td><td class="num">28</td>
|
||
<td>единый словарь типов: приведение устаревших значений, подписи, коды SR и каталог для API,
|
||
согласованность с критериями экспертной таблицы</td></tr>
|
||
<tr><td><code>tests/test_api_contract.py</code></td><td class="num">25</td>
|
||
<td>поля ответов, которые читает веб-интерфейс: базовый и детальный анализ, здоровье и карточка
|
||
модели, выгрузка XLSX, обработка ошибок; различимость метрик между снимками и валидность
|
||
PNG-визуализаций</td></tr>
|
||
</table>
|
||
|
||
<p>Отдельный контур — браузерные проверки интерфейса (<code>tests/browser/*.js</code>, Node и
|
||
<code>playwright-core</code>): они открывают интерфейс во временном профиле Chrome, загружают DICOM,
|
||
кликают по строкам таблицы и снимают содержимое панели деталей. Требуется запущенный сервер на
|
||
<code>127.0.0.1:8123</code>; профиль пользователя не затрагивается.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:28%">Сценарий</th><th>Что проверяет</th></tr>
|
||
<tr><td><code>ui_check.js</code></td>
|
||
<td>подсказку о кликабельности строк и её скрытие при пустом фильтре; открытие панели деталей из
|
||
строки; изменение значений при переключении строк; наполнение панели «О модели» метриками и
|
||
словарём</td></tr>
|
||
<tr><td><code>ui_violation.js</code></td>
|
||
<td>ветку «нарушение»: бейдж, <code>POOR</code>, <code>HIGH</code>, текст заключения</td></tr>
|
||
<tr><td><code>ui_offline.js</code></td>
|
||
<td>отсутствие обращений страницы к внешним хостам</td></tr>
|
||
</table>
|
||
|
||
<h2 id="s16">16. Известные ошибки и их обработка</h2>
|
||
|
||
<p>Требование задания — отсутствие необработанных исключений: сбой на одном файле фиксируется в отчёте и
|
||
не прерывает пакет. Ниже — фактические режимы отказа и способ обработки.</p>
|
||
|
||
<table>
|
||
<tr><th style="width:30%">Ситуация</th><th style="width:38%">Поведение</th><th>Где обрабатывается</th></tr>
|
||
<tr><td>Ошибка чтения или обработки файла (CLI)</td>
|
||
<td>строка получает <code>quality_class = -1</code>, область <code>unknown</code>, пустые UID и
|
||
<code>processing_status = Failure: <Тип>: <сообщение></code> (до 120 символов);
|
||
остальные файлы обрабатываются дальше</td>
|
||
<td><code>process_one</code>, <code>src/dxa/inference.py</code></td></tr>
|
||
<tr><td>Неподдерживаемая форма <code>pixel_array</code> (не 2D и не 3D)</td>
|
||
<td><code>ValueError</code>, превращается в <code>Failure:</code> согласно строке выше</td>
|
||
<td><code>preprocess.py</code></td></tr>
|
||
<tr><td>Чекпоинт отсутствует или не читается</td>
|
||
<td>сервис поднимается, <code>/api/v1/health</code> отдаёт <code>model_loaded: false</code>, а
|
||
запросы анализа — HTTP 500 <code>{"error": "Model not loaded"}</code></td>
|
||
<td><code>load_model()</code>, <code>src/main.py</code></td></tr>
|
||
<tr><td>Архитектура чекпоинта не совпала с запрошенной</td>
|
||
<td><code>ValueError</code> с указанием сохранённых <code>backbone</code> / <code>head</code></td>
|
||
<td><code>load_model</code>, <code>src/dxa/inference.py</code></td></tr>
|
||
<tr><td>Чекпоинт — «сырой» <code>state_dict</code> без метаданных</td>
|
||
<td><code>ValueError</code>: файл не содержит <code>model_state_dict</code></td>
|
||
<td><code>DXAQualityModel.load</code></td></tr>
|
||
<tr><td>Сбой в одиночном запросе <code>/analyze</code>, <code>/analyze/detailed</code>,
|
||
<code>/analyze/sr</code></td>
|
||
<td>HTTP 500 с телом <code>{"error": <текст>, "processing_status": "Failure: <первые 50
|
||
символов>"}</code>; детальный эндпоинт дополнительно печатает стек в лог сервера</td>
|
||
<td>обработчики маршрутов</td></tr>
|
||
<tr><td>Сбой одного файла в <code>/api/v1/batch</code></td>
|
||
<td>элемент становится <code>{"filename", "error", "processing_status": "Failure"}</code>,
|
||
остальные файлы возвращаются нормально</td>
|
||
<td><code>batch_analyze</code></td></tr>
|
||
<tr><td>Сбой одного файла в <code>/api/v1/export</code></td>
|
||
<td>строка со статусом <code>Failure: <до 80 символов></code> и <code>quality_class = -1</code>
|
||
попадает в XLSX, выгрузка не прерывается</td>
|
||
<td><code>export_results</code></td></tr>
|
||
<tr><td>Файл разметки не найден при обучении</td>
|
||
<td>предупреждение в лог и откат на метки из имён файлов; откат залогирован явно, потому что
|
||
вместе с источником меток меняются метрики</td>
|
||
<td><code>resolve_labels_csv</code>, <code>src/dxa/train.py</code></td></tr>
|
||
<tr><td>Пустое разбиение (нет обучающих или валидационных снимков)</td>
|
||
<td><code>RuntimeError</code> с числами <code>train</code> / <code>val</code> до начала обучения</td>
|
||
<td><code>train()</code></td></tr>
|
||
</table>
|
||
|
||
<div class="note">Сервис без чекпоинта намеренно не пытается «угадывать»: он поднимается, но честно
|
||
сообщает <code>model_loaded: false</code> и отклоняет запросы анализа. Молчаливая неверная оценка вместо
|
||
явной ошибки была бы опаснее в медицинском контуре.</div>
|
||
|
||
<h2 id="s17">17. Ограничения и достоверность результатов</h2>
|
||
|
||
<p>Ограничения сформулированы так, чтобы читатель мог оценить границы применимости решения, а не
|
||
только увидеть итоговые метрики.</p>
|
||
|
||
<ol>
|
||
<li><b>Разметка выведена из оценки исследования, а не снимка.</b> Экспертная таблица описывает
|
||
исследование; перенос вердикта на снимок однозначен (область встречается один раз), но
|
||
поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в качество
|
||
этой разметки, а не только в объём данных.</li>
|
||
<li><b>Мало данных.</b> 252 уникальных снимка, 77 нарушений. Доверительные интервалы широкие
|
||
(ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам), поэтому оценка на закрытом наборе может
|
||
отличаться.</li>
|
||
<li><b>Эталон — та же таблица.</b> Независимой истины нет, поэтому сравнение правил разметки
|
||
частично благоприятствует варианту «только таблица» (раздел 12.3).</li>
|
||
<li><b>В DICOM нет разметки областей интереса.</b> Ни overlay, ни graphic annotation в файлах нет,
|
||
поэтому корректность нанесённых областей измерения нельзя проверить прямым сравнением с
|
||
эталоном: оценивается геометрия видимой зоны.</li>
|
||
<li><b>Тип нарушения определяется признаками снимка, а не обученной моделью.</b> Модель решает только
|
||
бинарную задачу, поэтому в выгрузке тип содержателен только для размеченных снимков, а на
|
||
инференсе эвристика на текущих данных всегда даёт «не уточнён» (раздел 8.2). Для честного
|
||
мультикласса нужна разметка типов на уровне снимка; пять снимков имеют только код
|
||
«не уточнён», потому что источник не указывает критерий.</li>
|
||
<li><b>Три снимка размечены по пометке в имени файла</b> — экспертная таблица их область не
|
||
оценивала. Такие строки помечены признаком <code>filename_fallback</code>.</li>
|
||
<li><b>Сторона бедра в семи исследованиях с единственным снимком не проверяема</b>: теги
|
||
<code>Laterality</code> пусты, оценка взята из единственного заполненного столбца таблицы.</li>
|
||
<li><b>Порог определения области привязан к текущему оборудованию.</b> Признак «ширина кадра»
|
||
безошибочно работает на этом наборе, но при смене аппарата потребует калибровки.</li>
|
||
<li><b>Числовые эвристики панели деталей не калиброваны.</b> Их пороги рассчитаны на другой
|
||
масштаб интенсивностей, поэтому в интерфейсе показываются измерения, а не вердикты.</li>
|
||
<li><b>Рабочая точка порога даёт высокий recall при умеренной точности.</b> На обучающем наборе при
|
||
пороге, подобранном по F1, модель относит к нарушениям 341 строку из 548 (≈ 62 %) при
|
||
фактической доле нарушений 30.6 %, что согласуется с precision 0.486. Порог выбран в пользу
|
||
полноты: пропустить непригодное исследование дороже, чем показать лишнее.</li>
|
||
</ol>
|
||
|
||
<h2 id="s18">18. План развития</h2>
|
||
|
||
<ul>
|
||
<li>Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор — это снимает
|
||
главное ограничение (тип нарушения определяется признаками, а не моделью).</li>
|
||
<li>Расширить набор до 500+ исследований: доверительные интервалы сузятся, появится возможность
|
||
честной валидации без пересечения с обучением.</li>
|
||
<li>Заменить признак «ширина кадра» на калибровку по метаданным аппарата, чтобы определение области
|
||
не зависело от конкретного оборудования.</li>
|
||
<li>Добавить локализацию нарушения (тепловая карта или контур) — в задании это отнесено к
|
||
дополнительному функционалу, и для него нужна соответствующая разметка.</li>
|
||
<li>Собрать пилотную интеграцию с АРМ: отбор исследований, требующих внимания, и журналирование
|
||
подтверждений специалиста.</li>
|
||
</ul>
|
||
|
||
<h2 id="a1">Приложение А. Структура репозитория</h2>
|
||
|
||
<pre>bone_2026/
|
||
├── src/
|
||
│ ├── main.py FastAPI: маршруты и загрузка модели
|
||
│ ├── dxa/ действующий модуль оценки качества
|
||
│ │ ├── preprocess.py DICOM -> тензор (общий путь для обучения и API)
|
||
│ │ ├── model.py сеть, метрики, подбор порога, чекпоинт
|
||
│ │ ├── train.py обучение и отчёт (md/json)
|
||
│ │ ├── dataset.py DXADataset, DataLoader
|
||
│ │ ├── inference.py пакетный инференс, определение области, визуализация
|
||
│ │ ├── labels.py разбор имён, метки, склейка дублей, разбиение
|
||
│ │ ├── excel_labels.py разметка снимков по экспертной таблице
|
||
│ │ ├── rename_files.py приведение имён DICOM к единому виду
|
||
│ │ ├── violations.py единый словарь типов нарушений
|
||
│ │ ├── model_card.py карточка решения для API и интерфейса
|
||
│ │ ├── compare_labels.py сравнение источников разметки
|
||
│ │ ├── discriminator.py проверка вклада содержимого снимка
|
||
│ │ └── render.py рендер снимков и контактных листов
|
||
│ ├── quality/ эвристики: измерения снимка
|
||
│ ├── api/static/ веб-интерфейс (офлайн-ассеты)
|
||
│ └── utils/ выбор устройства
|
||
├── labels/ размеченные данные, разбиение, карта переименований
|
||
├── assets/ схемы, скриншоты, обоснование разметки
|
||
├── models/dxa_model.pth рабочий чекпоинт и отчёт обучения
|
||
├── tests/ pytest + браузерные проверки интерфейса
|
||
├── docs/ настоящий документ (HTML и PDF)
|
||
├── run.sh, Dockerfile, Dockerfile_cuda, Jenkinsfile, requirements.txt</pre>
|
||
|
||
<h2 id="a2">Приложение Б. Команды</h2>
|
||
|
||
<pre># обучение и разметка
|
||
./run.sh label разметить датасет по экспертной таблице
|
||
./run.sh rename план приведения имён файлов (--apply для применения)
|
||
./run.sh train обучить модель (режим меток по умолчанию)
|
||
./run.sh split && ./run.sh compare сравнить варианты разметки на одном разбиении
|
||
|
||
# инференс и сервис
|
||
./run.sh infer "dataset_hack/Для теста" results.xlsx
|
||
./run.sh serve API и веб-интерфейс на порту 8000
|
||
python -m src.dxa.discriminator проверка вклада содержимого снимка
|
||
|
||
# проверки
|
||
./run.sh test pytest (209 тестов)
|
||
node tests/browser/ui_check.js проверки интерфейса (нужен сервер на 127.0.0.1:8123)
|
||
|
||
# контейнер
|
||
docker build -t dxa-quality .
|
||
docker run -v /path/to/data:/data -v /path/to/models:/app/models -p 8000:8000 dxa-quality</pre>
|
||
|
||
<p class="sign">Документ подготовлен по фактическому состоянию репозитория; машинные отчёты обучения —
|
||
<code>models/train_report.json</code>, обоснование разметки — <code>assets/labeling.md</code>.</p>
|
||
|
||
</body>
|
||
</html>
|