embedding-analysis

Par mkurman · zorai

Calculez et analysez des embeddings pour évaluer la qualité de jeux de données, comparer des distributions, effectuer une déduplication sémantique, mesurer la diversité et filtrer par similarité. Couvre sentence-transformers, les diagnostics de l'espace d'embedding, et les techniques issues de la littérature 2025-2026 (déduplication sémantique NeMo Curator, métriques de similarité d'embedding pour la sélection de données, scoring de qualité de type DataRater).

npx skills add https://github.com/mkurman/zorai --skill embedding-analysis

Analyse d'Embedding pour la Curation de Dataset

Vue d'ensemble

Les embeddings transforment du texte/images non structurés en un espace vectoriel où la qualité, la diversité et la redondance deviennent mesurables. Cette skill couvre la curation de dataset basée sur les embeddings en utilisant sentence-transformers, des métriques de comparaison de distribution, la dédupléplication sémantique et des techniques de scoring de qualité issues de la littérature 2025-2026.

Quand utiliser

Utilisez cette skill quand :

  • Détecter des exemples quasi-dupliqués ou sémantiquement redondants.
  • Comparer les distributions d'embedding sur les splits du dataset (train vs. val vs. test).
  • Mesurer la diversité ou la couverture du dataset dans l'espace d'embedding.
  • Filtrer des exemples selon leur distance d'embedding à un ensemble de référence « gold ».
  • Appliquer une dédupléplication sémantique NeMo Curator à l'échelle.
  • Calculer des signaux de qualité inspirés de DataRater à partir des voisinages d'embedding.

Ne l'utilisez pas pour :

  • La dédupléplication de texte brut (exact match) — utilisez un simple hashing.
  • L'entraînement de modèles — utilisez les skills transformers ou trl.
  • La persistance de base de données vectorielle — utilisez les skills chromadb, milvus ou qdrant.

Installation

uv pip install sentence-transformers umap-learn scikit-learn numpy

Flux de travail principal

1. Générer les embeddings

from sentence_transformers import SentenceTransformer
import numpy as np

# Choisir le modèle selon le domaine
model = SentenceTransformer("all-MiniLM-L6-v2")  # rapide, généraliste
# model = SentenceTransformer("all-mpnet-base-v2")  # meilleure qualité
# model = SentenceTransformer("BAAI/bge-large-en-v1.5")  # optimisé pour retrieval
# model = SentenceTransformer("intfloat/multilingual-e5-large")  # multilingue

# Encodage par batch (mémoire-efficace)
embeddings = model.encode(
    texts,
    batch_size=64,
    show_progress_bar=True,
    normalize_embeddings=True,  # similarité cosinus via produit scalaire
    convert_to_numpy=True,
)

2. Dédupléplication sémantique

La dédupléplication sémantique supprime les exemples sémantiquement équivalents mais non identiques en texte. Inspirée par NeMo Curator (NVIDIA, 2024-2025) et la littérature académique.

Approche basée sur le clustering (NeMo Curator SemDedup)

from sklearn.cluster import MiniBatchKMeans
from sklearn.metrics.pairwise import cosine_similarity

def semantic_dedup(embeddings, threshold=0.95, n_clusters=100):
    """
    Cluster les embeddings, puis supprime les quasi-doublons dans chaque cluster.
    threshold: similarité cosinus au-delà de laquelle deux exemples sont des doublons.
    Retourne: masque booléen (True = garder, False = doublon).
    """
    n = len(embeddings)
    keep = np.ones(n, dtype=bool)

    # Clustering grossier pour l'efficacité (O(n * k) au lieu de O(n^2))
    k = min(n_clusters, n // 10)
    clusters = MiniBatchKMeans(n_clusters=k, random_state=42, batch_size=1024).fit_predict(embeddings)

    for c in range(k):
        idx = np.where(clusters == c)[0]
        if len(idx) < 2:
            continue
        sim = cosine_similarity(embeddings[idx])
        # Marquer les doublons (le plus bas index gardé, les plus hauts supprimés)
        for i in range(len(idx)):
            if not keep[idx[i]]:
                continue
            dupes = np.where(sim[i, i+1:] > threshold)[0]
            for d in dupes:
                keep[idx[i + 1 + d]] = False

    return keep

Approche par composantes connexes (LSHBloom, 2025)

# Pour la dédupléplication à l'échelle internet (>100M d'exemples) :
# Utiliser LSH pour les k-voisins approchés + union-find
from sklearn.neighbors import NearestNeighbors

def lsh_semantic_dedup(embeddings, threshold=0.90, n_neighbors=10):
    """Trouve les clusters d'exemples sémantiquement similaires via un graphe NN."""
    nn = NearestNeighbors(n_neighbors=n_neighbors, metric="cosine", n_jobs=-1)
    nn.fit(embeddings)
    distances, indices = nn.kneighbors(embeddings)

    # Union-find pour fusionner les composantes connexes
    parent = np.arange(len(embeddings))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x
    def union(a, b):
        parent[find(a)] = find(b)

    for i in range(len(embeddings)):
        for j, d in zip(indices[i], distances[i]):
            if i != j and (1 - d) > threshold:  # distance cosinus → similarité
                union(i, j)

    # Garder un élément par composante
    components = {}
    for i in range(len(embeddings)):
        root = find(i)
        if root not in components:
            components[root] = i

    return list(components.values())  # indices à conserver

3. Comparaison de distribution sur les splits

Comparer les distributions d'embedding entre train/val/test pour détecter la dérive ou les biais.

from scipy.spatial.distance import jensenshannon
from scipy.stats import wasserstein_distance

def embedding_distribution_shift(train_emb, test_emb, n_bins=50):
    """Quantifie la dérive entre les distributions d'embedding train et test."""

    # Projection en 1D pour la comparaison de distribution
    from sklearn.decomposition import PCA
    pca = PCA(n_components=1, random_state=42).fit(
        np.concatenate([train_emb, test_emb])
    )
    train_1d = pca.transform(train_emb).ravel()
    test_1d = pca.transform(test_emb).ravel()

    # Métriques basées sur l'histogramme
    hist_range = (min(train_1d.min(), test_1d.min()),
                   max(train_1d.max(), test_1d.max()))
    train_hist, _ = np.histogram(train_1d, bins=n_bins, range=hist_range, density=True)
    test_hist, _ = np.histogram(test_1d, bins=n_bins, range=hist_range, density=True)

    # Divergence Jensen-Shannon (0 = identique, 1 = maximalement différent)
    js_div = jensenshannon(train_hist + 1e-10, test_hist + 1e-10)

    # Distance de Wasserstein (Earth Mover's)
    w_dist = wasserstein_distance(train_1d, test_1d)

    return {"js_divergence": js_div, "wasserstein": w_dist}

def per_dimension_shift(train_emb, test_emb):
    """Dérive par dimension — identifie quels axes sémantiques ont dérivé."""
    train_mean = train_emb.mean(axis=0)
    test_mean = test_emb.mean(axis=0)
    cos_sim = np.dot(train_mean, test_mean) / (
        np.linalg.norm(train_mean) * np.linalg.norm(test_mean)
    )
    max_dim_shift = np.argmax(np.abs(train_mean - test_mean))
    return {"mean_cosine": cos_sim, "max_shift_dim": int(max_dim_shift)}

4. Scoring de qualité d'embedding (Inspiré de DataRater, 2025)

DataRater (Calian et al., NeurIPS 2025) meta-apprend une fonction de qualité sur les voisinages d'embedding. Voici une approximation pratique.

def embedding_quality_score(embeddings, k=20):
    """
    Score chaque exemple par la cohérence de son voisinage d'embedding.
    Faible cohérence → anomalie / bruit potentiel.
    Forte cohérence → in-distribution, probablement haute qualité.
    """
    nn = NearestNeighbors(n_neighbors=k + 1, metric="cosine")
    nn.fit(embeddings)
    distances, _ = nn.kneighbors(embeddings)

    # Exclure la distance à soi-même (index 0)
    mean_dist = distances[:, 1:].mean(axis=1)

    # Normaliser à [0, 1] — distance inférieure = qualité supérieure
    scores = 1 - (mean_dist - mean_dist.min()) / (mean_dist.max() - mean_dist.min() + 1e-10)
    return scores

5. Mesure de diversité

def embedding_diversity(embeddings, n_clusters=50):
    """
    Mesure la diversité du dataset via la couverture de clusters.
    Retourne: ratio de couverture et entropie des assignations de cluster.
    """
    k = min(n_clusters, len(embeddings) // 10)
    clusters = MiniBatchKMeans(n_clusters=k, random_state=42, batch_size=1024).fit_predict(embeddings)

    # Couverture de cluster : fraction de clusters non vides
    unique, counts = np.unique(clusters, return_counts=True)
    coverage = len(unique) / k

    # Entropie : plus élevé = distribution plus uniforme sur les clusters
    probs = counts / counts.sum()
    entropy = -np.sum(probs * np.log(probs + 1e-10)) / np.log(k)  # normalisé

    return {"coverage": coverage, "normalized_entropy": entropy}

def embedding_redundancy_score(embeddings, sample_size=5000):
    """
    Estime la redondance : similarité cosinus pairwise moyenne.
    Score élevé → dataset homogène ; nécessite une diversification.
    """
    idx = np.random.choice(len(embeddings), min(sample_size, len(embeddings)), replace=False)
    sample = embeddings[idx]
    sim = cosine_similarity(sample)
    # Exclure la diagonale
    mask = ~np.eye(len(sample), dtype=bool)
    return float(sim[mask].mean())

Filtrage basé sur la perplexité (GRAPE Score, 2025)

Utilisez un petit modèle de référence pour scorer la qualité des données par perplexité. Perplexité élevée = peu familier/difficile/bruyant.

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

def grape_score(texts, model_name="gpt2", batch_size=8):
    """
    Scoring de perplexité de style GRAPE.
    Perplexité inférieure → plus in-distribution, probablement qualité supérieure.
    Perplexité très élevée → charabia, bruit ou hors-domaine.
    """
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    model = AutoModelForCausalLM.from_pretrained(model_name).eval()

    scores = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        enc = tokenizer(batch, return_tensors="pt", padding=True, truncation=True, max_length=512)
        with torch.no_grad():
            outputs = model(**enc, labels=enc["input_ids"])
            loss = outputs.loss  # NLL moyen sur le batch
            ppl = torch.exp(loss).item()
            scores.append(ppl)

    return np.array(scores)

Quality Gate

Une passe de curation basée sur l'embedding est complète quand :

  • Le seuil de dédupléplication sémantique est justifié et documenté.
  • Les distributions d'embedding train/val/test sont comparées (divergence JS, Wasserstein).
  • Les métriques de diversité et redondance sont calculées avant et après curation.
  • Les scores d'anomalie sont examinés — la faible cohérence de voisinage peut indiquer du bruit ou des données rares mais précieuses.
  • Les résultats sont reproductibles (seeds fixes, versions de modèle sauvegardées).

Skills similaires