from __future__ import annotations
from typing import Sequence
import numpy as np
from sklearn.metrics.pairwise import rbf_kernel, laplacian_kernel, polynomial_kernel
from ..embed import resolve_embeddings
from .types import MeasureResult
from ..utility.validate import warn_on_zero_norm_rows
### Geometry-Based Diversity Measure
_KERNEL_TYPES = ("cs", "rbf", "lap", "poly")
[docs]
def log_determinant(
data: Sequence[Sequence[float]],
kernel_type: str = "cs",
tau: float = 1.0,
normalize: bool = True,
eps: float = 1e-6,
use_cholesky: bool = True,
*,
diversity_axis: str = "semantic",
embedding_model: str | None = None,
chunking_kwargs: dict | None = None,
) -> MeasureResult:
"""**Interpretation of values:** larger value = more diverse (can be negative; less negative = more diverse).
**Range:** unbounded in general, (-inf, +inf), depending on kernel choice.
For the default cosine kernel (``kernel_type="cs"``, ``tau=1.0``,
``normalize=True``), trace(K) = n bounds the value above by
``n * log(1 + eps)`` (approximately 0), so scores are negative in practice,
with magnitude driven by zero or near-zero eigenvalues (e.g. when n > d).
Compute Log-Determinant Diversity (LDD): the log-determinant of a kernel
matrix built from the input vectors, ``LDD = log det(K + eps * I)``. For a
positive definite kernel matrix this measures the "volume" spanned by the
data in the feature space, so larger values indicate greater diversity.
1) Build a similarity/kernel matrix K from the input vectors (per
``kernel_type``).
2) Add jitter to the diagonal for numerical stability: A = K + eps * I.
3) Return ``log det(A)`` (via Cholesky, falling back to ``slogdet``).
References:
Kulesza, A., & Taskar, B. (2012). Determinantal Point Processes for Machine Learning. Found. Trends Mach. Learn., 5, 123-286.
Wang, Peiqi, Yikang Shen, Zhen Guo, Matthew Stallone, Yoon Kim, Polina Golland, and Rameswar Panda. "Diversity measurement and subset selection for instruction tuning datasets." arXiv preprint arXiv:2402.02318 (2024).
Args:
data:
Iterable/array-like of (embedding) vectors with shape (n, d), or raw
text strings. Must contain at least 2 samples.
kernel_type:
Type of similarity/kernel:
- ``"cs"``: cosine-similarity-like (X Xᵀ / tau, optionally normalized).
- ``"rbf"``: RBF kernel (``rbf_kernel`` with ``gamma=tau``).
- ``"lap"``: Laplacian kernel (``laplacian_kernel`` with ``gamma=tau``).
- ``"poly"``: Polynomial kernel (``polynomial_kernel`` with ``degree=tau``).
tau:
Temperature / kernel parameter.
For "cs", the similarity matrix is (X Xᵀ) / tau.
For RBF / Laplacian it is passed as ``gamma=tau``,
for polynomial as ``degree=tau``.
normalize:
If True and kernel_type=="cs", L2-normalize the input vectors row-wise
before computing X Xᵀ, so dot product equals cosine similarity.
eps:
Jitter term added to the diagonal (eps * I) for numerical stability.
Makes the matrix more positive definite and prevents singular matrices.
use_cholesky:
If True, use Cholesky decomposition for efficient logdet computation
when the matrix is positive definite. Falls back to slogdet if Cholesky fails.
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
log-determinant of (K + eps * I) (higher = more diverse) and
``parameters`` records the configuration used.
Raises:
ValueError:
If there are fewer than 2 datapoints, or if tau <= 0, or if eps <= 0.
NotImplementedError:
For unknown kernel_type.
np.linalg.LinAlgError:
If the matrix determinant is not positive (sign <= 0) after adding eps.
Try increasing eps or re-checking kernel choice.
Warns:
UserWarning: If ``kernel_type="cs"`` and ``normalize=True`` and the
input contains an all-zero row (cosine similarity is undefined for
it). The score is still returned, treating the zero row as
near-orthogonal to every other point.
"""
data, embedding_model = resolve_embeddings(data, diversity_axis, embedding_model, measure="log_determinant", chunking_kwargs=chunking_kwargs)
parameters = {
"kernel_type": kernel_type,
"tau": tau,
"normalize": normalize,
"eps": eps,
"use_cholesky": use_cholesky,
"embedding_model": embedding_model,
}
# ---- Validate inputs ----
if kernel_type not in _KERNEL_TYPES:
raise NotImplementedError(
f"Unknown kernel_type '{kernel_type}'. Use one of: {_KERNEL_TYPES}."
)
if tau <= 0:
raise ValueError("tau must be positive")
if len(data) < 2:
raise ValueError("LDD requires at least 2 datapoints")
if eps <= 0:
raise ValueError("eps must be positive")
if kernel_type == "poly" and not float(tau).is_integer():
raise ValueError("For 'poly' kernel, tau must be an integer (degree).")
X = np.asarray(data, dtype=float)
if X.ndim != 2:
raise ValueError(f"Expected 2D array of shape (n, d), got shape {X.shape}")
# ---- 1) Build kernel matrix K ----
if kernel_type == "cs":
if normalize:
warn_on_zero_norm_rows(X, "log_determinant")
norms = np.linalg.norm(X, axis=1, keepdims=True)
norms = np.clip(norms, 1e-12, None)
X_use = X / norms
else:
X_use = X
K = (X_use @ X_use.T) / tau
elif kernel_type == "rbf":
K = rbf_kernel(X, X, gamma=tau)
elif kernel_type == "lap":
K = laplacian_kernel(X, X, gamma=tau)
else: # poly
K = polynomial_kernel(X, X, degree=int(tau))
# Symmetrize for safety (numerical noise)
K = 0.5 * (K + K.T)
n = K.shape[0]
A = K + eps * np.eye(n, dtype=K.dtype)
# ---- Compute logdet ----
if use_cholesky:
try:
L = np.linalg.cholesky(A)
return {"value": float(2.0 * np.sum(np.log(np.diag(L)))), "parameters": parameters}
except np.linalg.LinAlgError:
# fallback below
pass
sign, logdet = np.linalg.slogdet(A)
if sign <= 0:
# If this happens often, increase eps or re-check kernel choice.
raise np.linalg.LinAlgError(
"logdet undefined: det(A) is not positive (sign <= 0). "
"Try larger eps or re-check kernel choice."
)
return {"value": float(logdet), "parameters": parameters}