Статьи Обо мне

От хакатона до production — путь Scopiq

Я — DoctorM&Ai, врач-заведующий клинико-диагностической лабораторией (КДЛ) с 15+ годами за плечами в микроскопе и соло AI-разработчик. Каждый день я тону в мазках крови, мочи и спермы: лаборанты считают лейкоциты вручную, тратят по 10–15 минут на слайд, ошибаются на 15–20% из-за усталости или артефактов. А пациентов — очередь на неделю. В 2023-м я сказал "хватит этой херне" и на хакатоне слепил MVP Scopiq — AI для автоматизированного анализа микроскопических изображений.

С хакатона до production прошло 9 месяцев, 500+ часов кода, 3 переписанных архитектуры и куча фейлов. Сейчас Scopiq обрабатывает 1500+ слайдов в день в моей КДЛ, с точностью 97,8% на лейкоцитах и скоростью 8 секунд на слайд. Экономия — 80% времени лаборантов. Но путь был адским: от overfitting'а на 500 фото до Kubernetes-кошмаров на соло. Расскажу всё по-честному, с кодом, стеком и цифрами. Структура классика: проблема → решение → реализация → результат → выводы. Поехали!

Проблема: Почему ручной анализ в КДЛ — это пиздец

В КДЛ 80% времени уходит на рутинный морфологический анализ: подсчёт эритроцитов, лейкоцитов, эпителия в мазках крови, мочи, сперматозоидов. Нормы простые: в моче <5 лейкоцитов в поле зрения (п/з), в крови лейкоцитарная формула (ЛФ) по 100 клеткам.

Факты из моей практики (2022–2023): - Объём: 500–800 слайдов/день в средней КДЛ. - Время на слайд: 10–20 мин (лаборант смотрит 10–20 п/з под микроскопом ×100–400). - Ошибки: 12–18% (по внутренним аудитам). Причины: артефакты (кристаллы, слизь), переутомление (смены 12ч), дефицит кадров (в моей КДЛ уволилось 3/7 лаборантов за год). - Стоимость: 1 анализ ~500 руб, но задержки = потеря 20–30% выручки. - Цифры рынка: В РФ 5000+ КДЛ, глобально — $10B на ручной микроскопии (Statista 2023). AI в гематологии (как Sysmex) есть, но дорого ($100k+), не для мазков.

Я пробовал автоматизаторы вроде CellaVision — круто, но $50k/штука + калибровка под каждый микроскоп. Нужен дешёвый софт для веб-камеры на окуляре (стоимость ~5k руб). Проблема: изображения noisy (разные освещения, микроскопы), классы несбалансированы (эритроцитов 90%, патологии 1%).

Решение: Scopiq — AI-скоп в кармане

Scopiq — end-to-end пайплайн для анализа мазков: 1. Детекция объектов: Клетки/артефакты на слайде. 2. Классификация: Лейкоциты (нейтрофилы, лимфоциты и т.д.), эритроциты, эпителий, сперма, патологии (бактерии, кристаллы). 3. Подсчёт и отчёт: ЛФ, нормы, флаг аномалий. 4. Интеграция: Фото с телефона/вебки → анализ → PDF-отчёт.

MVP с хакатона: 85% accuracy на тесте. Production: 97,8% (F1-score). Ключ — transfer learning + domain adaptation под noisy данные КДЛ.

Задачи ML: - Object detection: YOLOv8 (быстро, mAP 0.92). - Segmentation: SAM (Segment Anything) для точных масок. - Classification: fine-tune ResNet50 (top-1 98%).

Цель: <10 сек/слайд, accuracy >95%, цена — 50 руб/анализ (cloud).

Реализация: От Streamlit-MVP до микросервисов

Этап 1: Хакатон (48 часов, ноябрь 2023, "AI in Med" — 1-е место из 50 команд)

Стек: минималистичный, соло. - Frontend: Streamlit (webapp за 50 строк). - Backend: Python 3.11, OpenCV 4.8, Keras 2.15 (TensorFlow 2.15). - ML: Собственная CNN на базе MobileNetV2 (input 512x512, 8 классов: neutro, lympho, mono, eosino, baso, erythro, epi, artifact). - Данные: 1200 фото с моего микроскопа (вебка Olympus + iPhone), аугментация (Albumentations: rotate, blur, contrast ±30%).

Код MVP (detektion + classif):

import streamlit as st
import cv2
import numpy as np
from tensorflow.keras.models import load_model
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
from tensorflow.keras.models import Model
import albumentations as A

# Загрузка модели (fine-tune MobileNet)
base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(512,512,3))
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(128, activation='relu')(x)
predictions = Dense(8, activation='softmax')(x)  # 8 классов
model = Model(inputs=base_model.input, outputs=predictions)
model.load_weights('hakatron_model.h5')  # accuracy 82% на val

@st.cache_resource
def load_model():
    return model

def preprocess(img):
    transform = A.Compose([
        A.Resize(512,512),
        A.Normalize(mean=0.485, std=0.229),  # ImageNet stats
        A.HorizontalFlip(p=0.5),
        A.RandomBrightnessContrast(p=0.3)
    ])
    return transform(image=img)['image']

st.title("Scopiq MVP")
uploaded = st.file_uploader("Загрузи слайд")
if uploaded:
    img = cv2.imdecode(np.frombuffer(uploaded.read(), np.uint8), 1)
    img_proc = preprocess(img)
    pred = model.predict(np.expand_dims(img_proc,0))[0]
    classes = ['нейтрофилы','лимфоциты','моноциты','эозинофилы','базофилы','эритроциты','эпителий','артефакт']
    counts = {c: int(p*100) for c,p in zip(classes, pred)}  # Dummy count
    st.bar_chart(counts)

Провал #1: Grid search на CPU — 12 часов, overfitting (train acc 98%, val 72%). Решение: dropout 0.5 + early stopping. На хакатоне accuracy 82% на 200 val фото, но путала артефакты с лимфо (F1=0.65).

Демо: 50 слайдов за 2 мин, судьи в шоке — 1-е место, приз 100k руб.

Этап 2: Прототип (1 месяц, декабрь 2023)

Переход на PyTorch + YOLOv8 (Ultralytics). - Данные: +5000 фото (собрал с коллег, Kaggle hemocell + synthetic via CutMix). - Тренировка: RTX 3060 (локально), 10 epochs, batch 16, lr=0.001 (AdamW). - Стек: FastAPI + Uvicorn, Celery+Redis для async inference, PostgreSQL (SQLAlchemy).

Архитектура прототипа:

[Telegram Bot / Web] --> FastAPI (upload img)
                    |
                    v
[Preprocess: OpenCV + Albumentations]
                    |
                    v
[YOLOv8 detect] --> crop cells --> [SAM segment] --> [ResNet50 classif]
                    |                                           |
                    v                                           v
[Postprocess: count + stats] --> PDF (ReportLab) --> [DB store]

Код YOLO training:

# data.yaml
train: /data/train
val: /data/val
nc: 8  # classes
names: ['neutro', 'lympho', ...]
from ultralytics import YOLO
model = YOLO('yolov8n.pt')  # nano для speed
model.train(data='data.yaml', epochs=50, imgsz=640, batch=32, device=0)
# Результат: mAP@0.5=0.85, inference 15ms/img на GPU

Провал #2: YOLO ловил клетки, но SAM (Meta) жрал 2GB VRAM и лагал на 1080p слайдах. Фикс: downscale to 1024x768 + ONNX export для CPU-fallback. Val mAP вырос с 0.78 до 0.91.

Deploy: Docker на VPS (Hetzner 4vCPU/16GB, 500руб/мес).

FROM python:3.11-slim
COPY . /app
RUN pip install -r requirements.txt  # fastapi, ultralytics, torch, onnxruntime
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Тестирование: 95 лаборантов, 2000 слайдов. Accuracy 89%, но false positives на slime (слизь) — 25%.

Этап 3: Production (январь–июль 2024, 5 итераций)

Полный стек: - Backend: FastAPI 0.104 (REST + WebSockets для live preview), Pydantic v2. - ML: PyTorch 2.1 + TorchServe (serving), MLflow (tracking: 150+ runs). - Queue: Celery 5.3 + Redis 7 + Flower dashboard. - DB: Postgres 15 + TimescaleDB (time-series для stats), MinIO (S3-like для imgs). - Frontend: HTMX + Alpine.js (no React, соло-friendly), Tailwind. - Deploy: Docker Compose → Kubernetes (k3s на 2-node cluster, DigitalOcean 20$/мес). CI/CD: GitHub Actions. - Мониторинг: Prometheus + Grafana, Sentry для ошибок.

Архитектура production:

Users (Web/Telegram/Mobile app)
          |
    Nginx (rate limit 100/min)
          |
   FastAPI Gateway (auth JWT)
 /     |     \
Preproc  ML Queue  DB
         |
    Celery Workers (YOLO+SAM+Classif)
         |
      TorchServe (GPU pod)
         |
        MinIO (store raw/preds)

Ключевой код: ML Pipeline (Celery task):

from celery import Celery
from ultralytics import YOLO
import onnxruntime as ort
import torch
from segment_anything import sam_model_registry

app = Celery('scopiq', broker='redis://localhost')

# Load models (singleton)
yolo_model = YOLO('scopiq_yolo.pt')
sam = sam_model_registry['vit_b']('sam_vitb.pt')
classif_sess = ort.InferenceSession('resnet50.onnx')

@app.task
def analyze_slide(image_path: str):
    img = cv2.imread(image_path)
    # Detect
    results = yolo_model(img, conf=0.5, verbose=False)
    boxes = results[0].boxes.xyxy.cpu().numpy()

    counts = {c: 0 for c in CLASSES}
    for box in boxes:
        crop = crop_cell(img, box)
        mask = sam_predict(sam, crop)  # SAM mask
        feat = extract_features(crop, mask)  # ResNet feats
        pred_class = classif_sess.run(None, {'input': feat})[0]
        counts[pred_class] += 1

    # Stats: LF = counts / sum(counts) * 100
    return generate_report(counts)

Тренировка prod-модели: - Dataset: 15k изображений (собранные + public: 8k, synthetic 7k via Diffusers StableDiffusion). - Aug: RandAug + MixUp (alpha=0.2). - Hyperparams: CosineAnnealingLR, SWA (Stochastic Weight Averaging). - Hardware: Colab Pro (A100) + локальный 3060. Время: 48h train, MLflow logged: loss 0.12, mAP 0.94.

Провалы и фиксы (честно): 1. Overfitting v1: Train 99%, prod 81%. Фикс: 5-fold CV, +3k OOF данных. Время: 2 недели. 2. Latency spikes: Celery queue >5s. Фикс: Gunicorn workers=4, Redis AOF persistence. P95 -> 3s. 3. GPU OOM в K8s: TorchServe жрал 12GB. Фикс: FP16 + Torch.compile (PyTorch 2.1), pod limits 8GB. 4. Drift: Модель путала новые микроскопы (Canon vs Olympus). Фикс: Active Learning — фидбек лаборантов в DB, retrain ежемесячно (LoRA adapters, +2% acc). 5. Безопасность: SQLi в early FastAPI. Фикс: Pydantic strict + bandit scans. 0 vulns в prod. 6. Scale fail: 500 req/min — K8s crash. Фикс: HPA (Horizontal Pod Autoscaler), от 2 до 10 pods.

Бюджет: 150k руб (hardware/cloud), ROI: окупаемость за 2 мес (экономия 2 лаборанта × 80k/мес).

Результат: Цифры не врут

Метрики ML (prod, 10k слайдов, август 2024): | Метрика | Хакатон | Прототип | Production | Цель | |---------|---------|----------|------------|------| | mAP@0.5 (YOLO) | 0.72 | 0.85 | 0.94 | >0.90 | | F1-classif | 0.78 | 0.89 | 0.978 | >0.95 | | Inference time | 2.5s | 1.2s | 0.8s (CPU)/0.12s (GPU) | <1s | | False Pos/Neg | 22% | 11% | 2.2% | <5% |

Бизнес-результаты: - Объём: 1500 слайдов/день (×3 от manual). - Точность: 97.8% совпадение с экспертами (kappa 0.95, audit 500 слайдов). - Экономия: 75% времени (15мин → 20сек + 10сек review). - Пользователи: Моя КДЛ + 5 партнёров (200+ анализов/день). - Revenue: 50 руб/анализ × 45k/мес = 2.25M руб/мес (маржа 80%). - Uptime: 99.7% (Grafana дашборд).

График drift (MLflow):

Loss: train 0.08 → prod 0.15 (стабильно после LoRA).

Отзывы: "Сопик спасает жопу в пики" — старший лаборант.

Выводы: Уроки соло-доктора-AI

Scopiq — не идеал, но работает. Что пошло круто: YOLO+SAM stack (SOTA speed/acc), соло-deploy K8s (экономия 500k на команде), data flywheel (feedback loop).

Честные фейлы: - Время: 9 мес вместо 3 (data collection — 40% усилий). - Баги: 150+ Sentry issues, 20% — edge cases (грязные слайды). - Стоимость: Cloud 30k/мес, но ROI 10x. - Масштаб: Пока РФ, экспорт тормозит FDA-like cert (нужен ISO 13485).

Уроки: 1. Data first: 80% успеха — датасет. Используй synthetic + active learning. 2. MVP → Prod: Streamlit → FastAPI+Celery = win. K8s соло possible, но start с Compose. 3. Мониторь drift: Z-score на predictions, retrain auto. 4. Соло-limits: No deep learning ops (используй no-code как Roboflow). 5. Медицина: Cert > всё. Тестируй на реальных 10k+ cases.

Будущее: Мобильное app (iOS/Android), интеграция с 1C-Мед, multi-lang. Если внедряешь — пиши, поделю репой (github.com/doctormai/scopiq, MIT license). Вопросы? Комменты

📋 Копировать для: