Annotation de Datasets ML : Méthodes et Outils Pratiques

Annotation de Datasets ML : Méthodes et Outils Pratiques

Vous êtes convaincu de la valeur d’un modèle IA entraîné sur vos données métier, mais vous sous-estimez peut-être la partie la plus chronophage du projet : l’annotation. Selon un rapport Cognilytica, 80 % du temps d’un projet machine learning est consacré à la préparation et à l’annotation des données, et non à l’entraînement du modèle lui-même. Pourtant, c’est l’étape la moins glamour, celle dont on parle le moins dans les articles techniques. Ce guide pratique démystifie l’annotation de datasets ML : méthodes, outils, stratégies pour travailler efficacement, et comment l’IA peut accélérer l’annotation elle-même.

Pourquoi l’annotation est la clé d’un modèle performant

Le modèle ML apprend à partir des exemples que vous lui fournissez. Si vos annotations sont incohérentes, incomplètes ou biaisées, votre modèle le sera aussi — c’est le principe fondamental “garbage in, garbage out”.

Un dataset annoté de qualité, c’est :

  • Des labels cohérents : deux annotateurs différents doivent donner le même label au même exemple (mesurable avec l’accord inter-annotateurs, ou IAA)
  • Des données représentatives : couvrir les cas rares et les cas limites, pas seulement les cas faciles
  • Une définition claire des classes : des guidelines d’annotation précises qui éliminent l’ambiguïté
  • Une bonne couverture : suffisamment d’exemples par classe pour que le modèle généralise

Types d’annotation selon le problème ML

Le type d’annotation dépend entièrement de votre objectif :

Tâche MLType d’annotationComplexitéCoût relatif
Classification d’imagesLabel par imageFaible
Détection d’objetsBounding boxesMoyen€€
Segmentation sémantiqueMasques pixel par pixelÉlev退€
NER (entités nommées)Spans de texte + labelMoyen€€
Classification de texteLabel par documentFaible
Résumé de textePaires texte/résuméÉlev退€
RLHF (alignment LLM)Comparaisons de réponsesTrès élev退€€

Outils d’annotation : panorama

Label Studio — le plus polyvalent (open source)

Label Studio est l’outil de référence pour les équipes qui veulent garder le contrôle de leurs données. Il supporte tous les types d’annotation : texte, image, audio, vidéo, même des données structurées.

# Installation et démarrage
pip install label-studio
label-studio start

# Ou avec Docker pour une installation propre
docker run -it -p 8080:8080 -v $(pwd)/mydata:/label-studio/data 
    heartexlabs/label-studio:latest

Configuration d’un projet de NER (Named Entity Recognition) via l’interface :

<!-- Template de labeling pour la reconnaissance d'entités nommées -->
<View>
  <Labels name="label" toName="text">
    <Label value="ORGANISATION" background="#FFA39E"/>
    <Label value="PERSONNE" background="#D4380D"/>
    <Label value="LIEU" background="#FFC069"/>
    <Label value="DATE" background="#AD8B00"/>
    <Label value="MONTANT" background="#7CB305"/>
  </Labels>
  
  <Text name="text" value="$text"/>
</View>

Import/export via API :

import requests

LABEL_STUDIO_URL = "http://localhost:8080"
API_KEY = "your-api-key"

headers = {"Authorization": f"Token {API_KEY}"}

# Import de données à annoter
def import_texts_to_label_studio(project_id: int, texts: list[str]):
    tasks = [{"data": {"text": t}} for t in texts]
    
    response = requests.post(
        f"{LABEL_STUDIO_URL}/api/projects/{project_id}/import",
        headers=headers,
        json=tasks
    )
    return response.json()

# Export des annotations
def export_annotations(project_id: int, export_format: str = "JSON") -> list[dict]:
    response = requests.get(
        f"{LABEL_STUDIO_URL}/api/projects/{project_id}/export",
        headers=headers,
        params={"exportType": export_format}
    )
    return response.json()

CVAT — spécialisé vision par ordinateur

Pour les tâches de vision (bounding boxes, segmentation, keypoints), CVAT (Computer Vision Annotation Tool) de Intel est la référence :

# Installation avec Docker Compose
git clone https://github.com/opencv/cvat
cd cvat
docker compose up -d
# Interface disponible sur http://localhost:8080

Roboflow — annotation + augmentation + export

Roboflow va plus loin que l’annotation : il intègre l’augmentation de données et l’export dans tous les formats (YOLO, COCO, Pascal VOC, TFRecord).

from roboflow import Roboflow

rf = Roboflow(api_key="votre-api-key")
project = rf.workspace("votre-workspace").project("votre-projet")

# Télécharger le dataset dans le format YOLO
dataset = project.version(1).download("yolov8")

# Entraîner directement (exemple avec YOLOv8)
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
model.train(data=f"{dataset.location}/data.yaml", epochs=50)

Annotation semi-automatisée : l’IA au service de l’annotation

L’approche la plus efficace aujourd’hui : utiliser un modèle IA pré-entraîné pour générer des annotations préliminaires, puis faire corriger par des humains. On passe ainsi de “annoter de zéro” à “valider et corriger”, ce qui est 3 à 5 fois plus rapide.

Pre-annotation avec un LLM (pour le texte)

import anthropic
import json
from typing import NamedTuple

client = anthropic.Anthropic()

class AnnotationCandidate(NamedTuple):
    text: str
    entities: list[dict]  # [{"start": int, "end": int, "label": str, "text": str}]
    confidence: float

def pre_annotate_ner(text: str, entity_types: list[str]) -> AnnotationCandidate:
    """
    Génère des annotations NER préliminaires via LLM.
    Un annotateur humain corrige ensuite.
    """
    entity_list = ", ".join(entity_types)
    
    response = client.messages.create(
        model="claude-haiku-4-5",  # Haiku pour économiser sur le coût d'annotation
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"""Identifie les entités nommées dans ce texte.
Types d'entités à détecter : {entity_list}

Texte : "{text}"

Réponds avec un JSON :
{{
  "entities": [
    {{"text": "texte de l'entité", "label": "TYPE", "start": position_début, "end": position_fin}}
  ],
  "confidence": score entre 0 et 1
}}

Les positions start/end sont des indices de caractères dans le texte original.
Réponds uniquement avec le JSON."""
        }]
    )
    
    result = json.loads(response.content[0].text)
    
    return AnnotationCandidate(
        text=text,
        entities=result.get("entities", []),
        confidence=result.get("confidence", 0.5)
    )

# Utilisation sur un batch
def pre_annotate_batch(texts: list[str], entity_types: list[str]) -> list[AnnotationCandidate]:
    candidates = []
    for text in texts:
        candidate = pre_annotate_ner(text, entity_types)
        candidates.append(candidate)
    return candidates

# Filtrage par confiance — envoyer seulement les cas incertains à la révision humaine
def smart_routing(candidates: list[AnnotationCandidate]) -> tuple[list, list]:
    auto_approved = [c for c in candidates if c.confidence >= 0.90]
    needs_review = [c for c in candidates if c.confidence < 0.90]
    
    print(f"Auto-approuvé : {len(auto_approved)} ({len(auto_approved)/len(candidates)*100:.0f}%)")
    print(f"À réviser : {len(needs_review)}")
    
    return auto_approved, needs_review

Active Learning : annoter intelligemment

L’active learning est une stratégie qui maximise la valeur de chaque annotation humaine. Au lieu d’annoter aléatoirement, on sélectionne en priorité les exemples les plus informatifs pour le modèle.

import numpy as np
from sklearn.base import BaseEstimator

def uncertainty_sampling(model: BaseEstimator, unlabeled_data: np.ndarray, 
                          n_samples: int = 100) -> np.ndarray:
    """
    Sélectionne les exemples les plus incertains (entropie maximale).
    Ces exemples apporteront le plus d'information au modèle.
    """
    probabilities = model.predict_proba(unlabeled_data)
    
    # Calcul de l'entropie pour chaque exemple
    # Entropie maximale = modèle le plus incertain
    entropy = -np.sum(probabilities * np.log(probabilities + 1e-10), axis=1)
    
    # Indices des exemples les plus incertains
    most_uncertain = np.argsort(entropy)[-n_samples:]
    
    return most_uncertain

def diversity_sampling(embeddings: np.ndarray, already_annotated: np.ndarray,
                       n_samples: int = 100) -> np.ndarray:
    """
    Sélectionne les exemples les plus différents du corpus déjà annoté.
    Assure une bonne couverture de l'espace des données.
    """
    from sklearn.metrics.pairwise import cosine_distances
    
    # Distance minimale de chaque exemple non annoté aux exemples annotés
    distances = cosine_distances(embeddings, already_annotated)
    min_distances = distances.min(axis=1)
    
    # On sélectionne les plus éloignés (les plus différents)
    most_diverse = np.argsort(min_distances)[-n_samples:]
    
    return most_diverse

Contrôle qualité des annotations

Mesurer l’accord inter-annotateurs (IAA)

Si plusieurs annotateurs travaillent sur le même corpus, il faut mesurer leur cohérence :

from sklearn.metrics import cohen_kappa_score
import numpy as np

def calculate_iaa(annotations_a: list, annotations_b: list) -> dict:
    """
    Calcule le kappa de Cohen entre deux annotateurs.
    Score > 0.8 : accord excellent
    Score 0.6-0.8 : bon accord
    Score 0.4-0.6 : accord modéré — réviser les guidelines
    Score < 0.4 : désaccord — formation nécessaire
    """
    kappa = cohen_kappa_score(annotations_a, annotations_b)
    
    if kappa > 0.8:
        quality = "Excellent"
    elif kappa > 0.6:
        quality = "Bon"
    elif kappa > 0.4:
        quality = "Modéré — réviser les guidelines"
    else:
        quality = "Insuffisant — formation requise"
    
    # Analyse des désaccords
    disagreements = [
        i for i, (a, b) in enumerate(zip(annotations_a, annotations_b)) 
        if a != b
    ]
    
    return {
        "kappa": float(kappa),
        "quality": quality,
        "agreement_rate": 1 - len(disagreements) / len(annotations_a),
        "disagreement_indices": disagreements[:10]  # 10 premiers désaccords pour analyse
    }

Détection d’anomalies dans les annotations

from collections import Counter

def audit_annotations(annotations: list[dict]) -> dict:
    """Détecte les patterns suspects dans un corpus annoté."""
    
    issues = []
    
    # Distribution des labels
    labels = [a["label"] for a in annotations]
    label_counts = Counter(labels)
    total = len(labels)
    
    # Déséquilibre de classes extrême (< 2% d'une classe)
    for label, count in label_counts.items():
        percentage = count / total * 100
        if percentage < 2:
            issues.append({
                "type": "class_imbalance",
                "severity": "warning",
                "message": f"Classe '{label}' très rare ({percentage:.1f}%) — risque de biais"
            })
    
    # Annotateur trop rapide (moins de 5 secondes par annotation)
    fast_annotations = [
        a for a in annotations 
        if a.get("annotation_time_ms", float('inf')) < 5000
    ]
    if len(fast_annotations) / total > 0.3:
        issues.append({
            "type": "speed_anomaly",
            "severity": "critical",
            "message": f"{len(fast_annotations)/total*100:.0f}% d'annotations en moins de 5 secondes"
        })
    
    # Label toujours identique (annotateur fainéant)
    if len(label_counts) == 1:
        issues.append({
            "type": "single_label",
            "severity": "critical", 
            "message": "Tous les exemples ont le même label — annotation suspecte"
        })
    
    return {
        "total_annotations": total,
        "label_distribution": dict(label_counts),
        "issues": issues,
        "quality_score": max(0, 100 - len(issues) * 20)
    }

Guidelines d’annotation : la clé souvent négligée

Les guidelines sont le document qui définit ce que les annotateurs doivent faire dans chaque cas. Un mauvais guideline génère des données inutiles.

Structure minimale d’un bon guideline :

  1. Objectif du modèle : à quoi servira le modèle entraîné ? Cette question doit guider chaque décision d’annotation.
  2. Définition des classes : définition précise de chaque classe, avec exemples positifs ET négatifs.
  3. Cas limites traités : les 20 cas ambigus les plus fréquents, avec la règle de décision.
  4. Convention de tie-break : que faire en cas de doute ? (préférer la classe la plus spécifique, ou la plus générale ?)
  5. Exemples annotés : 10-20 exemples de référence, annotés par un expert.

Estimation du budget annotation

Pour une PME qui démarre un projet ML :

Type de projetVolume minimal viableCoût annotation interneCoût outsourcing
Classif. texte (2-5 classes)2 000 exemples20-30h500-1 500 €
NER (5-10 entités)5 000 phrases50-80h2 000-5 000 €
Classif. images3 000 images15-25h300-1 000 €
Détection objets2 000 images40-80h2 000-6 000 €

Avec la pre-annotation LLM, divisez ces estimations par 3 à 5.

Ce qu’il faut retenir

L’annotation de données n’est pas une tâche à sous-traiter et oublier — c’est un investissement stratégique dans la qualité de votre IA. Trois principes guident les projets réussis :

  1. Des guidelines béton avant de commencer : 2 heures de rédaction de guidelines évitent 20 heures de réannotation.
  2. Mesurer l’IAA systématiquement : un kappa en dessous de 0.6 signale un problème à régler avant de continuer.
  3. L’IA accélère l’annotation, pas la remplace : la pre-annotation LLM + validation humaine est le rapport qualité/vitesse optimal aujourd’hui.

Vous lancez un projet ML et avez besoin d’accompagnement sur la stratégie de données ? Brio Novia vous aide à concevoir votre pipeline d’annotation, choisir les bons outils, et définir des guidelines efficaces. Contactez-nous à contact@brio-novia.eu pour démarrer du bon pied.