Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Compute and analyze embeddings for dataset quality, distribution comparison, semantic deduplication, diversity measurement, and similarity-based filtering. Covers sentence-transformers, embedding space diagnostics, and 2025-2026 literature techniques (NeMo Curator semantic dedup, embedding similarity metrics for data selection, DataRater-style quality scoring).
.claude/skills/mkurman-embedding-analysis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 95% | 0% |
Embeddings turn unstructured text/images into a vector space where quality, diversity, and redundancy become measurable. This skill covers embedding-based dataset curation using sentence-transformers, distribution comparison metrics, semantic deduplication, and quality scoring techniques from the 2025-2026 literature.
Use this skill when:
Do not use for:
transformers or trl skills.chromadb, milvus, or qdrant skills.bashuv pip install sentence-transformers umap-learn scikit-learn numpy
pythonfrom sentence_transformers import SentenceTransformer import numpy as np # Choose model based on domain model = SentenceTransformer("all-MiniLM-L6-v2") # fast, general # model = SentenceTransformer("all-mpnet-base-v2") # higher quality # model = SentenceTransformer("BAAI/bge-large-en-v1.5") # retrieval-optimized # model = SentenceTransformer("intfloat/multilingual-e5-large") # multilingual # Batched encoding (memory-efficient) embeddings = model.encode( texts, batch_size=64, show_progress_bar=True, normalize_embeddings=True, # cosine similarity via dot product convert_to_numpy=True, )
Semantic dedup removes examples that are meaning-equivalent but not text-identical. From NeMo Curator (NVIDIA, 2024-2025) and academic literature.
pythonfrom sklearn.cluster import MiniBatchKMeans from sklearn.metrics.pairwise import cosine_similarity def semantic_dedup(embeddings, threshold=0.95, n_clusters=100): """ Cluster embeddings, then remove near-duplicates within each cluster. threshold: cosine similarity above which two examples are duplicates. Returns: boolean mask (True = keep, False = duplicate). """ n = len(embeddings) keep = np.ones(n, dtype=bool) # Coarse clustering for efficiency (O(n * k) instead of 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]) # Mark duplicates (lower-index kept, higher-index removed) 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
python# For internet-scale dedup (>100M examples): # Use LSH for approximate nearest neighbor + union-find from sklearn.neighbors import NearestNeighbors def lsh_semantic_dedup(embeddings, threshold=0.90, n_neighbors=10): """Finds clusters of semantically similar examples via NN graph.""" nn = NearestNeighbors(n_neighbors=n_neighbors, metric="cosine", n_jobs=-1) nn.fit(embeddings) distances, indices = nn.kneighbors(embeddings) # Union-find to merge connected components 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: # cosine dist → similarity union(i, j) # Keep one per component components = {} for i in range(len(embeddings)): root = find(i) if root not in components: components[root] = i return list(components.values()) # indices to keep
Compare embedding distributions between train/val/test to detect drift or bias.
pythonfrom scipy.spatial.distance import jensenshannon from scipy.stats import wasserstein_distance def embedding_distribution_shift(train_emb, test_emb, n_bins=50): """Quantify shift between train and test embedding distributions.""" # Project to 1D for distribution comparison 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() # Histogram-based metrics 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) # Jensen-Shannon divergence (0 = identical, 1 = maximally different) js_div = jensenshannon(train_hist + 1e-10, test_hist + 1e-10) # Wasserstein (Earth Mover's) distance w_dist = wasserstein_distance(train_1d, test_1d) return {"js_divergence": js_div, "wasserstein": w_dist} def per_dimension_shift(train_emb, test_emb): """Per-dimension mean shift — identifies which semantic axes drifted.""" 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)}
DataRater (Calian et al., NeurIPS 2025) meta-learns a quality function over embedding neighborhoods. Below is a practical approximation.
pythondef embedding_quality_score(embeddings, k=20): """ Score each example by embedding neighborhood coherence. Low coherence → outlier / potential noise. High coherence → in-distribution, likely high quality. """ nn = NearestNeighbors(n_neighbors=k + 1, metric="cosine") nn.fit(embeddings) distances, _ = nn.kneighbors(embeddings) # Exclude self-distance (index 0) mean_dist = distances[:, 1:].mean(axis=1) # Normalize to [0, 1] — lower distance = higher quality scores = 1 - (mean_dist - mean_dist.min()) / (mean_dist.max() - mean_dist.min() + 1e-10) return scores
pythondef embedding_diversity(embeddings, n_clusters=50): """ Measure dataset diversity via cluster coverage. Returns: coverage ratio and entropy of cluster assignments. """ k = min(n_clusters, len(embeddings) // 10) clusters = MiniBatchKMeans(n_clusters=k, random_state=42, batch_size=1024).fit_predict(embeddings) # Cluster coverage: fraction of clusters that are non-empty unique, counts = np.unique(clusters, return_counts=True) coverage = len(unique) / k # Entropy: higher = more uniform distribution across clusters probs = counts / counts.sum() entropy = -np.sum(probs * np.log(probs + 1e-10)) / np.log(k) # normalized return {"coverage": coverage, "normalized_entropy": entropy} def embedding_redundancy_score(embeddings, sample_size=5000): """ Estimate redundancy: average pairwise cosine similarity. High score → dataset is homogeneous; needs diversification. """ idx = np.random.choice(len(embeddings), min(sample_size, len(embeddings)), replace=False) sample = embeddings[idx] sim = cosine_similarity(sample) # Exclude diagonal mask = ~np.eye(len(sample), dtype=bool) return float(sim[mask].mean())
Use a small reference model to score data quality by perplexity. High perplexity = unfamiliar/hard/noisy.
pythonfrom transformers import AutoModelForCausalLM, AutoTokenizer import torch def grape_score(texts, model_name="gpt2", batch_size=8): """ GRAPE-style perplexity scoring. Lower perplexity → more in-distribution, likely higher quality. Very high perplexity → gibberish, noise, or out-of-domain. """ 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 # average NLL across batch ppl = torch.exp(loss).item() scores.append(ppl) return np.array(scores)
An embedding-based curation pass is complete when:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | pass→fail | 17,918 | 16,985 | -5% | 1 | 1 | 0% | 3,573 | 6,354 | +78% | 0 | 0 | — |
case-01 | fail→pass | 16,475 | 12,534 | -24% | 1 | 1 | 0% | 2,981 | 5,410 | +81% | 0 | 0 | — |
case-02 | fail→fail | 18,211 | 12,539 | -31% | 1 | 1 | 0% | 3,674 | 5,402 | +47% | 0 | 0 | — |
case-03 | fail→pass | 20,940 | 15,431 | -26% | 1 | 1 | 0% | 4,083 | 5,794 | +42% | 0 | 0 | — |
case-04 | pass→pass | 10,970 | 9,835 | -10% | 1 | 1 | 0% | 2,360 | 4,684 | +98% | 0 | 0 | — |
case-05 | pass→pass | 19,404 | 10,458 | -46% | 1 | 1 | 0% | 4,099 | 4,932 | +20% | 0 | 0 | — |
case-06 | pass→pass | 11,462 | 9,992 | -13% | 1 | 1 | 0% | 2,528 | 4,996 | +98% | 0 | 0 | — |
case-07 | fail→pass | 12,716 | 12,891 | +1% | 1 | 1 | 0% | 2,512 | 5,236 | +108% | 0 | 0 | — |
case-08 | fail→fail | 16,069 | 10,806 | -33% | 1 | 1 | 0% | 3,237 | 5,022 | +55% | 0 | 0 | — |
case-09 | pass→pass | 11,378 | 10,032 | -12% | 1 | 1 | 0% | 2,375 | 4,680 | +97% | 0 | 0 | — |
case-11 | pass→pass | 7,832 | 4,139 | -47% | 1 | 1 | 0% | 1,380 | 3,495 | +153% | 0 | 0 | — |
case-12 | fail→pass | 14,682 | 3,871 | -74% | 1 | 1 | 0% | 2,285 | 3,378 | +48% | 0 | 0 | — |
case-13 | pass→pass | 5,886 | 3,833 | -35% | 1 | 1 | 0% | 1,055 | 3,355 | +218% | 0 | 0 | — |
case-14 | fail→pass | 12,870 | 5,761 | -55% | 1 | 1 | 0% | 1,941 | 3,782 | +95% | 0 | 0 | — |
case-15 | pass→fail | 12,201 | 8,999 | -26% | 1 | 1 | 0% | 1,973 | 4,185 | +112% | 0 | 0 | — |
case-16 | pass→pass | 13,175 | 10,494 | -20% | 1 | 1 | 0% | 2,247 | 4,474 | +99% | 0 | 0 | — |
case-17 | fail→pass | 13,325 | 12,210 | -8% | 1 | 1 | 0% | 2,203 | 5,086 | +131% | 0 | 0 | — |
case-18 | pass→pass | 10,584 | 6,463 | -39% | 1 | 1 | 0% | 1,765 | 3,821 | +116% | 0 | 0 | — |
case-19 | pass→pass | 24,010 | 7,288 | -70% | 1 | 1 | 0% | 2,176 | 4,089 | +88% | 0 | 0 | — |
case-20 | pass→pass | 20,840 | 15,637 | -25% | 1 | 1 | 0% | 3,500 | 5,783 | +65% | 0 | 0 | — |
case-21 | pass→pass | 21,508 | 17,200 | -20% | 1 | 1 | 0% | 4,165 | 6,039 | +45% | 0 | 0 | — |
case-22 | pass→pass | 17,530 | 13,409 | -24% | 1 | 1 | 0% | 3,665 | 5,618 | +53% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.