Платформа розмітки даних: коли типові рішення не підходять
Анотування даних часто стає вузьким горлечком ML-проєкту. Будуючи платформу для класифікації медичних висновків, замовник розмічав 500 документів на тиждень руками трьох експертів. Після впровадження кастомної платформи з автоматичною передрозміткою та Active Learning пропускна здатність зросла до 3000 документів на тиждень при тих же трьох анотаторах — у 6 разів ефективніше, ніж ручний процес. Економія на оплаті праці склала $12 000 на місяць.
Типові рішення на кшталт Label Studio або Supervisely не завжди закривають специфіку: потрібна інтеграція зі своєю моделлю, нестандартні типи розмітки (ієрархічна класифікація з 10 000+ класів), контроль якості через IAA, передрозмітка слабкими моделями або замкнений Active Learning. За 10+ проєктів ми набили ґулі: від черг, що відвалюються на 50K завдань, до розсинхронізації анотацій у реальному часі. Гарантуємо якість та дотримання термінів завдяки багаторічному досвіду.
Як Active Learning скорочує витрати на розмітку?
У типовому проєкті NLP або Computer Vision анотування даних займає 60-80% часу. Ручний процес без пайплайнів призводить до трьох головних проблем: дублювання завдань (один документ йде двом анотаторам без агрегації), простої розмітників через ручний розподіл і систематичний пропуск складних прикладів — модель навчається на легких кейсах, а на бойових даних падає. Платформа вирішує це єдиним API: інґрест → передрозмітка → черга → контроль → експорт → Active Learning. Платформа з Active Learning дає приріст продуктивності в 3-5 разів порівняно з ручною розміткою. Пропускна здатність зростає в 3-5 разів при тій же кількості людей. При вартості години анотатора ~$15 це дає економію $4000–$8000 на місяць на команду з 5 осіб. Вартість базової платформи починається від $15 000, повнофункціональної — від $45 000.
Чому кастомна платформа краща за типові рішення?
Контроль якості без ручних перевірок. Типова ситуація: два анотатори розмічають один текст, але розходяться в 30% випадків. Без IAA ви не знаєте, хто правий. Ми впроваджуємо Cohen's Kappa (класифікація) та F1-узгодженість (NER), автоматично відправляємо на рев'ю спірні завдання. Поріг якості налаштовується під проєкт — зазвичай 0.8-0.85.
Передрозмітка знижує трудовитрати на 40-70%. Використовуємо слабкі моделі: zero-shot NLI від Facebook (bart-large-mnli) для класифікації або GLiNER для NER. Якщо впевненість передбачення вище 0.85 — завдання автоматично приймається, анотатор тільки підтверджує. Наші тести на датасеті з 10K документів показали: 60% завдань проходять авто-валідацію з точністю 97%.
Active Learning — модель сама обирає, що розмічати. Стратегія uncertainty: обираємо приклади з максимальною ентропією передбачень. Це дає приріст якості моделі на 5-10% порівняно з випадковою вибіркою. Для production використовуємо гібрид: 70% невизначеність + 30% різноманіття (core-set), щоб не застрягати на схожих прикладах. Детальніше про Active Learning читайте в Wikipedia.
Архітектура платформи
[Raw Data Sources] ↓ [Ingestion & Preprocessing] ← конвертація форматів, deduplification ↓ [Pre-annotation (weak models)] ← економія 40-70% ↓ [Task Queue Management] ← розподіл ↓ [Annotation Interface] ← Label Studio / custom UI ↓ [Quality Control] ← IAA, gold standard ↓ [Export & Model Training] ← JSONL, COCO, YOLO ↓ [Active Learning Loop] ← складні приклади Ключові модулі платформи
Управління завданнями та анотаторами
from anthropic import Anthropic import pandas as pd from enum import Enum from dataclasses import dataclass, field from datetime import datetime import uuid import numpy as np class TaskStatus(Enum): PENDING = "pending" PRE_ANNOTATED = "pre_annotated" IN_REVIEW = "in_review" COMPLETED = "completed" DISPUTED = "disputed" @dataclass class AnnotationTask: task_id: str data: dict # raw data (text, image_url, etc.) task_type: str # classification, ner, segmentation annotations: list = field(default_factory=list) pre_annotation: dict = None status: TaskStatus = TaskStatus.PENDING assigned_to: list = field(default_factory=list) created_at: datetime = field(default_factory=datetime.now) difficulty_score: float = 0.5 class AnnotationPlatform: def __init__(self, db_connection): self.db = db_connection self.llm = Anthropic() self.quality_threshold = 0.8 # Minimum IAA self.annotators_per_task = 2 def ingest_data(self, raw_data: list[dict], task_type: str) -> list[AnnotationTask]: """Прийом даних та створення завдань""" tasks = [] for item in raw_data: task = AnnotationTask( task_id=str(uuid.uuid4()), data=item, task_type=task_type ) tasks.append(task) # Попередня оцінка складності tasks = self._estimate_difficulty(tasks) # Пріоритизація: спочатку легкі для швидкого старту tasks.sort(key=lambda t: t.difficulty_score) return tasks def _estimate_difficulty(self, tasks: list[AnnotationTask]) -> list[AnnotationTask]: """LLM-оцінка складності завдань для пріоритизації""" # Батч-оцінка через LLM sample_texts = [t.data.get('text', '')[:200] for t in tasks[:20]] if not any(sample_texts): return tasks text_list = "\n".join([f"{i+1}. {t}" for i, t in enumerate(sample_texts)]) response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=300, messages=[{ "role": "user", "content": f"""Rate the annotation difficulty of these texts (0-1, where 1 is hardest). Consider: ambiguity, domain specificity, length complexity. Texts: {text_list} Return only comma-separated scores, e.g.: 0.3, 0.7, 0.5...""" }] ) try: scores = [float(s.strip()) for s in response.content[0].text.split(',')] for i, task in enumerate(tasks[:len(scores)]): task.difficulty_score = scores[i] except Exception: pass return tasks Контроль якості через IAA
def compute_iaa(self, annotations: list[dict], task_type: str) -> float: """ Inter-Annotator Agreement: - Classification: Cohen's Kappa - NER: F1 agreement - Segmentation: IoU agreement """ if len(annotations) < 2: return 1.0 if task_type == 'classification': return self._cohen_kappa(annotations) elif task_type == 'ner': return self._ner_agreement(annotations) else: return self._pairwise_agreement(annotations) def _cohen_kappa(self, annotations: list[dict]) -> float: """Cohen's Kappa для класифікації""" from sklearn.metrics import cohen_kappa_score if len(annotations) == 2: labels_a = [a['label'] for a in annotations[0]['items']] labels_b = [a['label'] for a in annotations[1]['items']] если len(labels_a) != len(labels_b): return 0.0 try: return cohen_kappa_score(labels_a, labels_b) except Exception: return 0.0 return 0.5 # Default для >2 анотаторів (потрібен Fleiss kappa) def _ner_agreement(self, annotations: list[dict]) -> float: """F1 agreement для іменованих сутностей""" if len(annotations) < 2: return 1.0 spans_a = set( (e['start'], e['end'], e['label']) for e in annotations[0].get('entities', []) ) spans_b = set( (e['start'], e['end'], e['label']) for e in annotations[1].get('entities', []) ) if not spans_a and not spans_b: return 1.0 intersection = spans_a & spans_b if not intersection: return 0.0 precision = len(intersection) / len(spans_b) recall = len(intersection) / len(spans_a) f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 return f1 def review_disputed_task(self, task: AnnotationTask, annotations: list[dict]) -> dict: """Розбір спірних випадків через LLM""" response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=400, messages=[{ "role": "user", "content": f"""You are a senior annotation expert. Resolve this labeling dispute. Task type: {task.task_type} Text: {task.data.get('text', '')[:500]} Annotator A: {annotations[0]} Annotator B: {annotations[1]} Provide: 1. Correct annotation 2. Brief reasoning (1-2 sentences) 3. Guideline clarification needed (if any)""" }] ) return { 'resolution': response.content[0].text, 'resolved_by': 'llm_arbitration', 'task_id': task.task_id } Автоматична передрозмітка
class PreAnnotationEngine: """Передрозмітка для зниження навантаження анотаторів""" def __init__(self, task_type: str): self.task_type = task_type self.weak_model = None self.confidence_threshold = 0.85 # Тільки висококонфіденційні прийняти без рев'ю def pre_annotate_classification(self, texts: list[str], labels: list[str]) -> list[dict]: """Zero-shot класифікація через NLI""" from transformers import pipeline if self.weak_model is None: self.weak_model = pipeline( "zero-shot-classification", model="facebook/bart-large-mnli", device=0 ) results = [] batch_size = 32 for i in range(0, len(texts), batch_size): batch = texts[i:i + batch_size] preds = self.weak_model(batch, candidate_labels=labels, batch_size=batch_size) for pred in preds: top_label = pred['labels'][0] confidence = pred['scores'][0] results.append({ 'label': top_label, 'confidence': confidence, 'auto_accepted': confidence >= self.confidence_threshold }) return results def pre_annotate_ner(self, texts: list[str]) -> list[dict]: """NER через GLiNER (general NER)""" from gliner import GLiNER if self.weak_model is None: self.weak_model = GLiNER.from_pretrained("urchade/gliner_multi-v2.1") entity_types = ["person", "organization", "location", "date", "product"] results = [] for text in texts: entities = self.weak_model.predict_entities(text, entity_types) results.append({ 'entities': [ {'start': e['start'], 'end': e['end'], 'label': e['label'], 'confidence': e['score']} for e in entities ], 'auto_accepted': all(e['score'] >= self.confidence_threshold for e in entities) }) return results Active Learning цикл
class ActiveLearningLoop: """Розумний вибір наступних завдань для розмітки""" def select_informative_samples(self, unlabeled_pool: list[dict], current_model, strategy: str = 'uncertainty', budget: int = 100) -> list[int]: """ Стратегії: - uncertainty: найменш впевнені передбачення - diversity: найбільш різноманітні за feature space - hybrid: комбінація обох """ texts = [item.get('text', '') for item in unlabeled_pool] if strategy == 'uncertainty': probs = current_model.predict_proba(texts) # Найбільша ентропія = найбільша невизначеність entropy = -np.sum(probs * np.log(probs + 1e-10), axis=1) return np.argsort(entropy)[-budget:].tolist() elif strategy == 'diversity': # Core-set: максимально різноманітні приклади embeddings = current_model.encode(texts) # якщо є encoder selected = [np.random.randint(len(texts))] for _ in range(budget - 1): dists = np.min( np.linalg.norm( embeddings[:, None] - embeddings[selected], axis=2 ), axis=1 ) selected.append(np.argmax(dists)) return selected return list(range(min(budget, len(unlabeled_pool)))) Порівняння стратегій розмітки
| Стратегія | Витрати на 1K документів | IAA (класифікація) | Час виконання | Приріст якості моделі |
|---|---|---|---|---|
| Ручна розмітка | $750 (50 годин × $15) | 0.82 | 2 дні | База |
| Передрозмітка + рев'ю | $300 (20 годин × $15) | 0.88 | 1 день | +3% |
| Active Learning | $150 (10 годин × $15) | 0.91 | 0.5 дня | +5-10% |
Порівняння платформ: типові vs кастомна
| Характеристика | Label Studio (типове) | Кастомна платформа |
|---|---|---|
| Інтеграція з моделлю | Через хуки, обмежено | Повна інтеграція з вашим ML pipeline |
| Типи розмітки | Обмежено набором | Будь-які (ієрархія, 3D, відео) |
| Active Learning | Немає вбудованого | Вбудований цикл з uncertainty/diversity |
| Контроль якості | Базовий IAA | Cohen's Kappa, F1, LLM-арбітраж |
| Продуктивність | До 10K завдань/день | 50K+ завдань/день з оптимізацією |
Приклад розрахунку економії на команді з 5 анотаторів
Без платформи: 5 анотаторів × 40 годин/тиждень × $15/година = $3,000/тиждень. За місяць — $12,000. З Active Learning: обсяг розмітки скорочується в 3–5 разів, трудовитрати падають до $150–$300 на 1K документів. Підсумкова економія — $4,000–$8,000/міс.
Як впровадити платформу розмітки?
- Аудит даних і типів розмітки — визначаємо допустимі типи, складність, частоту помилок.
- Вибір стеку та архітектури — вирішуємо, які компоненти кастомізувати (Label Studio або з нуля), які моделі передрозмітки використовувати.
- Розробка бекенду — FastAPI + Celery + RabbitMQ для черг, PostgreSQL для зберігання.
- Інтеграція передрозмітки та Active Learning — підключаємо weak моделі та стратегію невизначеності.
- Налаштування контролю якості — пороги IAA, золотий стандарт, LLM-арбітраж.
- Тестування з реальними даними — навантажувальне тестування черг, перевірка узгодженості.
- Деплой та навчання команди — розгортаємо на вашій інфраструктурі, передаємо документацію.
Що входить у розробку та терміни
- API оркестрації — прийом даних, черга, пріоритизація, розподіл.
- Інтерфейс анотатора — кастомізований Label Studio або React UI.
- Модуль передрозмітки — слабкі моделі з порогами впевненості.
- Контроль якості — IAA, золотий стандарт, пайплайн рев'ю.
- Експорт — JSONL, COCO, YOLO, інтеграція з HuggingFace Datasets.
- Active Learning — обчислювач невизначеності та різноманіття.
- Документація та навчання команди.
Базова платформа на основі Label Studio — від 2 тижнів. Повнофункціональна з передрозміткою та Active Learning — від 3 до 8 тижнів залежно від складності. Вартість розраховується індивідуально після аудиту ваших даних та вимог.
Обговоріть ваш проєкт з інженерами — ми оцінимо дані та запропонуємо архітектуру. Напишіть нам, щоб отримати консультацію.







