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
transformersoutrl. - La persistance de base de données vectorielle — utilisez les skills
chromadb,milvusouqdrant.
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).