Source code for emb_diversity.measures.bins_entropy

from __future__ import annotations

import warnings

import numpy as np
from sklearn.decomposition import PCA

from ..embed import resolve_embeddings
from .types import MeasureResult

### Distribution-Based Diversity Measure


[docs] def bins_entropy( data, n_bins_x: int = 5, n_bins_y: int = 5, normalize: bool = True, normalization: str = "effective", # "effective" -> log(min(n,B)), "bins" -> log(B) projection: str = "umap", pca_kwargs=None, umap_kwargs=None, *, diversity_axis: str = "semantic", embedding_model: str | None = None, chunking_kwargs: dict | None = None, ) -> MeasureResult: """**Interpretation of values:** larger value = more diverse. **Range:** [0, 1] with the default ``"effective"`` normalization; with ``"bins"`` it may be < 1. Compute bins-based entropy diversity from a 2D projection of a vector set. 1) Project the input vectors to 2D with UMAP or PCA. 2) Bin points into a n_bins_x × n_bins_y grid. 3) Compute Shannon entropy over bin occupancies. 4) Optionally normalize. References: Cox, Samuel Rhys, Yunlong Wang, Ashraf Abdul, Christian von der Weth, and Brian Y. Lim. “Directed Diversity: Leveraging Language Embedding Distances for Collective Creativity in Crowd Ideation.” Proceedings of the 2021 CHI Conference on Human Factors in Computing Systems, May 6, 2021, 1–35. https://doi.org/10.1145/3411764.3445782. Yang, Yuming, Yang Nan, Junjie Ye, Shihan Dou, Xiao Wang, Shuo Li, Huijie Lv, Tao Gui, Qi Zhang, and Xuan-Jing Huang. "Measuring data diversity for instruction tuning: A systematic analysis and a reliable metric." In Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), pp. 18530-18549. 2025. Args: data: Iterable/array-like of (embedding) vectors with shape (n, d). Must contain at least 2 samples. Accepts numpy arrays and (optionally) torch tensors. n_bins_x: Number of bins along x-axis. Must be > 0. n_bins_y: Number of bins along y-axis. Must be > 0. normalize: If True, normalize entropy by a log factor. normalization: Normalization denominator: - "effective": (default) divide by log(min(n, B)); ensures result in [0, 1]. - "bins": divide by log(B) (paper-style; max < 1 when n < B). projection: "umap" or "pca". Defaults to "umap". pca_kwargs: Extra kwargs passed to PCA(...). Defaults to None (treated as {}). PCA is deterministic for full SVD solver. umap_kwargs: Extra kwargs passed to UMAP(...). Defaults to None (treated as {}). If random_state is not provided, random_state=42 is used. diversity_axis: Registered axis used to embed text input (default "semantic"). embedding_model: Explicit embedding model id; overrides *diversity_axis*. Returns: A dict ``{"value": float, "parameters": {...}}`` where ``value`` is the bins-based entropy (normalized to [0, 1] if normalize=True with the default effective normalization) and ``parameters`` records the configuration used. Raises: ImportError: If projection is "umap" but UMAP is not installed. ValueError: If input shape, bins, projection, or normalization is invalid. """ data, embedding_model = resolve_embeddings(data, diversity_axis, embedding_model, measure="bins_entropy", chunking_kwargs=chunking_kwargs) # Normalize input to numpy array X = np.asarray(data, dtype=float) if X.size == 0: raise ValueError("Cannot compute bins_based_entropy for fewer than 2 datapoints") if X.ndim != 2: raise ValueError(f"Expected 2D array of shape (n, d), got shape {X.shape}") n, d = X.shape if n < 2: raise ValueError("Cannot compute bins_based_entropy for fewer than 2 datapoints") if n_bins_x <= 0 or n_bins_y <= 0: raise ValueError("n_bins_x and n_bins_y must be positive integers") total_bins = int(n_bins_x) * int(n_bins_y) if projection not in {"umap", "pca"}: raise ValueError('projection must be either "umap" or "pca"') # UMAP's spectral initialization needs more points than its output # dimensionality and fails with cryptic internal errors below this. if projection == "umap" and n < 4: raise ValueError( f"bins_entropy with projection='umap' requires at least 4 " f"datapoints (got {n}); use projection='pca' for very small " "inputs or provide more data." ) # Projection kwargs if pca_kwargs is None: pca_kwargs = {} else: pca_kwargs = dict(pca_kwargs) # copy if umap_kwargs is None: umap_kwargs = {} else: umap_kwargs = dict(umap_kwargs) # copy parameters = { "n_bins_x": n_bins_x, "n_bins_y": n_bins_y, "normalize": normalize, "normalization": normalization, "projection": projection, "pca_kwargs": pca_kwargs, "umap_kwargs": umap_kwargs, "embedding_model": embedding_model, } # 2D projection # (User can still override solver/whiten/etc via kwargs) if projection == "umap": # umap is slow to import, so it is loaded only when actually # projecting with it (PCA calls never pay for it). try: from umap import UMAP except ImportError: raise ImportError("UMAP is not installed.") from ._umap import fit_transform_umap umap_kwargs.setdefault("random_state", 42) reducer = UMAP(n_components=2, **umap_kwargs) Y = fit_transform_umap(reducer, X) else: reducer = PCA(n_components=2, **pca_kwargs) Y = reducer.fit_transform(X) # shape (n, 2) # Compute bounds and ranges for binning min_x, min_y = Y.min(axis=0) max_x, max_y = Y.max(axis=0) range_x = max_x - min_x range_y = max_y - min_y # Assign each point to a bin eps = 1e-10 if range_x <= 0: bin_x = np.zeros(n, dtype=int) else: bin_x = np.floor((Y[:, 0] - min_x) / (range_x + eps) * n_bins_x).astype(int) if range_y <= 0: bin_y = np.zeros(n, dtype=int) else: bin_y = np.floor((Y[:, 1] - min_y) / (range_y + eps) * n_bins_y).astype(int) bin_x = np.clip(bin_x, 0, n_bins_x - 1) bin_y = np.clip(bin_y, 0, n_bins_y - 1) # Map 2D bins to 1D labels bin_labels = bin_x * n_bins_y + bin_y # Count occurrences in each occupied bin _, counts = np.unique(bin_labels, return_counts=True) # Empirical distribution over occupied bins f_b = counts / n # Shannon entropy over occupied bins (empty bins contribute 0) entropy = -np.sum(f_b * np.log(f_b)) # Optional normalization if normalize: if total_bins <= 1: # With a 1x1 grid, entropy is always 0; avoid division by zero return {"value": 0.0, "parameters": parameters} if normalization not in {"effective", "bins"}: raise ValueError('normalization must be either "effective" or "bins"') denom_bins = min(n, total_bins) if normalization == "effective" else total_bins denom = np.log(denom_bins) if denom <= 0: return {"value": 0.0, "parameters": parameters} entropy = entropy / denom # Numerical safety: keep in [0,1] for effective normalization if normalization == "effective": entropy = float(np.clip(entropy, 0.0, 1.0)) return {"value": float(entropy), "parameters": parameters}