Metrics Reconstruction#

reconstruction #

FUNCTION DESCRIPTION
reconstruction_error

Compute reconstruction error between original X and reconstructed data

relative_reconstruction_error

Compute relative reconstruction error normalized by data magnitude.

procrustes_error

Compute Procrustes error after optimal orthogonal alignment.

Functions#

reconstruction_error(X: torch.Tensor, X_reconstructed: torch.Tensor, reduce: str = 'mse', per_sample: bool = False) -> torch.Tensor #

Compute reconstruction error between original X and reconstructed data X_reconstructed.

PARAMETER DESCRIPTION
X

Original data, shape (n_samples, n_features).

TYPE: Tensor

X_reconstructed

Reconstructed data, shape (n_samples, n_features).

TYPE: Tensor

reduce

Reduction operation: "mse", "mae", "frobenius".

TYPE: str, optional, by default "mse" DEFAULT: 'mse'

per_sample

If True, return per-sample errors.

TYPE: bool, optional, by default False DEFAULT: False

RETURNS DESCRIPTION
Tensor

Reconstruction error.

Source code in spectre/metrics/reconstruction.py
def reconstruction_error(
    X: torch.Tensor,
    X_reconstructed: torch.Tensor,
    reduce: str = "mse",
    per_sample: bool = False,
) -> torch.Tensor:
    """
    Compute reconstruction error between original `X` and reconstructed data
    `X_reconstructed`.

    Parameters
    ----------
    X : torch.Tensor
        Original data, shape (n_samples, n_features).

    X_reconstructed : torch.Tensor
        Reconstructed data, shape (n_samples, n_features).

    reduce : str, optional, by default "mse"
        Reduction operation: "mse", "mae", "frobenius".

    per_sample : bool, optional, by default False
        If True, return per-sample errors.

    Returns
    -------
    torch.Tensor
        Reconstruction error.
    """
    check_2d(X)
    check_2d(X_reconstructed)
    check_same_shape(X, X_reconstructed)
    check_same_device(X, X_reconstructed)

    diff = X - X_reconstructed

    if reduce == "mse":
        # Mean squared error per sample
        error = (diff**2).mean(dim=1)

    elif reduce == "mae":
        # Mean absolute error per sample
        error = diff.abs().mean(dim=1)

    elif reduce == "frobenius":
        # Frobenius norm per sample (L2 norm)
        error = torch.norm(diff, p="fro", dim=1)

    else:
        raise ValueError(
            f"Unknown reduce operation '{reduce}'. Options: 'mse', 'mae', 'frobenius'."
        )

    if per_sample:
        return error
    else:
        return error.mean()

relative_reconstruction_error(X: torch.Tensor, X_reconstructed: torch.Tensor, eps: float = torch.finfo(torch.float32).eps, per_sample: bool = False) -> torch.Tensor #

Compute relative reconstruction error normalized by data magnitude.

PARAMETER DESCRIPTION
X

Original data, shape (n_samples, n_features).

TYPE: Tensor

X_reconstructed

Reconstructed data, shape (n_samples, n_features).

TYPE: Tensor

eps

Small constant for numerical stability.

TYPE: float, optional, by default torch.finfo(torch.float32).eps DEFAULT: eps

per_sample

If True, return per-sample errors.

TYPE: bool, optional, by default False DEFAULT: False

RETURNS DESCRIPTION
Tensor

Relative reconstruction error in [0, inf). Values closer to 0 are better.

Source code in spectre/metrics/reconstruction.py
def relative_reconstruction_error(
    X: torch.Tensor,
    X_reconstructed: torch.Tensor,
    eps: float = torch.finfo(torch.float32).eps,
    per_sample: bool = False,
) -> torch.Tensor:
    """
    Compute relative reconstruction error normalized by data magnitude.

    Parameters
    ----------
    X : torch.Tensor
        Original data, shape (n_samples, n_features).

    X_reconstructed : torch.Tensor
        Reconstructed data, shape (n_samples, n_features).

    eps : float, optional, by default torch.finfo(torch.float32).eps
        Small constant for numerical stability.

    per_sample : bool, optional, by default False
        If True, return per-sample errors.

    Returns
    -------
    torch.Tensor
        Relative reconstruction error in [0, inf). Values closer to 0 are better.
    """
    check_2d(X)
    check_2d(X_reconstructed)
    check_same_shape(X, X_reconstructed)
    check_same_device(X, X_reconstructed)

    if not isinstance(eps, float):
        raise TypeError(f"Expected eps to be float, got {type(eps).__name__}.")
    check_in_interval(eps, "[0, inf)")

    # Compute Frobenius norm per sample
    diff_norm = torch.norm(X - X_reconstructed, p="fro", dim=1)
    X_norm = torch.norm(X, p="fro", dim=1)

    # Relative error with numerical stability
    error = diff_norm / (X_norm + eps)

    if per_sample:
        return error
    else:
        return error.mean()

procrustes_error(X: torch.Tensor, Y: torch.Tensor, eps: float = torch.finfo(torch.float32).eps, scaling: bool = False) -> torch.Tensor #

Compute Procrustes error after optimal orthogonal alignment.

Finds optimal orthogonal transformation (rotation and optional scaling) to align Y with X, then computes the residual error. Useful for comparing embeddings that may differ by rotation.

PARAMETER DESCRIPTION
X

Target matrix, shape (n_samples, n_features).

TYPE: Tensor

Y

Source matrix to align, shape (n_samples, n_features).

TYPE: Tensor

scaling

If True, allow scaling in addition to rotation.

TYPE: bool, optional, by default False DEFAULT: False

eps

Small value to avoid division by zero.

TYPE: float, optional, by default torch.finfo(torch.float32).eps DEFAULT: eps

RETURNS DESCRIPTION
Tensor

Procrustes error after optimal alignment.

Examples:

>>> X = torch.randn(100, 3)
>>> # Rotate Y relative to X
>>> angle = torch.pi / 4
>>> R = torch.tensor(
...     [
...         [torch.cos(angle), -torch.sin(angle), 0],
...         [torch.sin(angle), torch.cos(angle), 0],
...         [0, 0, 1],
...     ]
... )
>>> Y = X @ R.T
>>> procrustes_error(X, Y)
0.0...

With noise and scaling:

>>> Y = 1.5 * (X @ R.T) + 0.01 * torch.randn(100, 3)
>>> procrustes_error(X, Y, scaling=True)
0.01...
Source code in spectre/metrics/reconstruction.py
def procrustes_error(
    X: torch.Tensor,
    Y: torch.Tensor,
    eps: float = torch.finfo(torch.float32).eps,
    scaling: bool = False,
) -> torch.Tensor:
    """
    Compute Procrustes error after optimal orthogonal alignment.

    Finds optimal orthogonal transformation (rotation and optional scaling)
    to align Y with X, then computes the residual error. Useful for comparing
    embeddings that may differ by rotation.

    Parameters
    ----------
    X : torch.Tensor
        Target matrix, shape (n_samples, n_features).

    Y : torch.Tensor
        Source matrix to align, shape (n_samples, n_features).

    scaling : bool, optional, by default False
        If True, allow scaling in addition to rotation.

    eps : float, optional, by default torch.finfo(torch.float32).eps
        Small value to avoid division by zero.

    Returns
    -------
    torch.Tensor
        Procrustes error after optimal alignment.

    Examples
    --------
    >>> X = torch.randn(100, 3)
    >>> # Rotate Y relative to X
    >>> angle = torch.pi / 4
    >>> R = torch.tensor(
    ...     [
    ...         [torch.cos(angle), -torch.sin(angle), 0],
    ...         [torch.sin(angle), torch.cos(angle), 0],
    ...         [0, 0, 1],
    ...     ]
    ... )
    >>> Y = X @ R.T
    >>> procrustes_error(X, Y)
    0.0...

    With noise and scaling:

    >>> Y = 1.5 * (X @ R.T) + 0.01 * torch.randn(100, 3)
    >>> procrustes_error(X, Y, scaling=True)
    0.01...
    """
    check_2d(X)
    check_2d(Y)
    check_same_shape(X, Y)
    check_same_device(X, Y)

    n_samples, n_features = X.shape

    X_centered = X - X.mean(dim=0, keepdim=True)
    Y_centered = Y - Y.mean(dim=0, keepdim=True)

    # Compute optimal rotation via SVD
    # X = Y R, so R = (Y^T Y)^{-1} Y^T X
    # Using SVD: Y^T X = U S V^T, then R = U V^T
    try:
        U, S, Vt = torch.linalg.svd(Y_centered.T @ X_centered, full_matrices=False)
        R = U @ Vt
    except RuntimeError:
        raise ValueError("SVD failed during Procrustes alignment.")

    # Apply rotation
    Y_aligned = Y_centered @ R

    if scaling:
        # Optimal scaling: s = tr(X^T Y_aligned) / tr(Y_aligned^T Y_aligned)
        numerator = torch.sum(X_centered * Y_aligned)
        denominator = torch.sum(Y_aligned * Y_aligned)
        if denominator > eps:
            scale = numerator / denominator
        else:
            scale = 1.0
        Y_aligned = scale * Y_aligned

    # Compute residual error
    error = torch.norm(X_centered - Y_aligned, p="fro") ** 2 / n_samples

    return error