# Copyright 2021 Lawrence Livermore National Security, LLC and other MuyGPyS
# Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: MIT
"""Distance functions
Compute pairwise and crosswise distance tensors for the purposes of kernel
construction.
See the following example computing the pairwise and crosswise distances between
a batch of training data and their nearest neighbors.
Example:
>>> from MuyGPyS.neighbors import NN_Wrapper
>>> from MuyGPyS.optimize.batch import sample_batch
>>> from MuyGPyS.gp.distance import crosswise_distances
>>> train_features = load_train_features()
>>> nn_count = 10
>>> nbrs_lookup = NN_Wrapper(
... train_features, nn_count, nn_method="exact", algorithm="ball_tree"
... )
>>> train_count, _ = train_features.shape
>>> batch_count = 50
>>> batch_indices, batch_nn_indices = sample_batch(
... nbrs_lookup, batch_count, train_count
... )
>>> pairwise_dists = pairwise_distances(
... train_features, batch_nn_inidices, metric="l2"
... )
>>> crosswise_dists = crosswise_distances(
... train_features,
... train_features,
... batch_indices,
... batch_nn_indices,
... metric="l2",
... )
)
See also the following example computing the crosswise distances between a
test dataset and their nearest neighors in the training data.
Example:
>>> from MuyGPyS.neighbors import NN_Wrapper
>>> from MuyGPyS.gp.distance import crosswise_distances
>>> train_features = load_train_features()
>>> test_features = load_test_features()
>>> nn_count = 10
>>> nbrs_lookup = NN_Wrapper(
... train_features, nn_count, nn_method="exact", algorithm="ball_tree"
... )
>>> nn_indices, nn_dists = nbrs_lookup.get_nns(test_features)
>>> test_count, _ = test_features.shape
>>> indices = np.arange(test_count)
>>> nn_indices, _ = nbrs_lookup.get_nns(test_features)
>>> pairwise_dists = pairwise_distances(
... train_features, nn_inidices, metric="l2"
... )
>>> crosswise_dists = crosswise_distances(
... test_features,
... train_features,
... indices,
... nn_indices,
... metric="l2"
... )
The helper functions :func:`MuyGPyS.gp.distance.make_regress_tensors` and
:func:`MuyGPyS.gp.distance.make_train_tensors` wrap these distances tensors and
also return the nearest neighbors sets' training targets and (in the latter
case) the training targets of the training batch. These functions are convenient
as the distance and target tensors are usually needed together.
"""
import numpy as np
from typing import Optional, Tuple
from sklearn.metrics.pairwise import cosine_similarity
[docs]def make_regress_tensors(
metric: str,
batch_indices: np.ndarray,
batch_nn_indices: np.ndarray,
test_features: np.ndarray,
train_features: np.ndarray,
train_targets: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Create the distance and target tensors for regression.
Creates the `crosswise_dists`, `pairwise_dists` and `batch_nn_targets`
tensors required by :func:`MuyGPyS.gp.MuyGPyS.regress`.
Args:
metric:
The metric to be used to compute distances.
batch_indices:
A vector of integers of shape `(batch_count,)` identifying the
training batch of observations to be approximated.
batch_nn_indices:
A matrix of integers of shape `(batch_count, nn_count)` listing the
nearest neighbor indices for all observations in the batch.
test_features:
The full floating point testing data matrix of shape
`(test_count, feature_count)`.
train_features:
The full floating point training data matrix of shape
`(train_count, feature_count)`.
train_targets:
A matrix of shape `(train_count, feature_count)` whose rows are
vector-valued responses for each training element.
Returns
-------
crosswise_dists:
A matrix of shape `(batch_count, nn_count)` whose rows list the distance
of the corresponding batch element to each of its nearest neighbors.
pairwise_dists:
A tensor of shape `(batch_count, nn_count, nn_count,)` whose latter two
dimensions contain square matrices containing the pairwise distances
between the nearest neighbors of the batch elements.
batch_nn_targets:
Tensor of floats of shape `(batch_count, nn_count, response_count)`
containing the expected response for each nearest neighbor of each batch
element.
"""
if test_features is None:
test_features = train_features
crosswise_dists = crosswise_distances(
test_features,
train_features,
batch_indices,
batch_nn_indices,
metric=metric,
)
pairwise_dists = pairwise_distances(
train_features, batch_nn_indices, metric=metric
)
batch_nn_targets = train_targets[batch_nn_indices, :]
return crosswise_dists, pairwise_dists, batch_nn_targets
[docs]def make_train_tensors(
metric: str,
batch_indices: np.ndarray,
batch_nn_indices: np.ndarray,
train_features: np.ndarray,
train_targets: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""
Create the distance and target tensors needed for training.
Similar to :func:`~MuyGPyS.gp.data.make_regress_tensors` but returns the
additional `batch_targets` matrix, which is only defined for a batch of
training data.
Args:
metric:
The metric to be used to compute distances.
batch_indices:
A vector of integers of shape `(batch_count,)` identifying the
training batch of observations to be approximated.
batch_nn_indices:
A matrix of integers of shape `(batch_count, nn_count)` listing the
nearest neighbor indices for all observations in the batch.
train_features:
The full floating point training data matrix of shape
`(train_count, feature_count)`.
train_targets:
A matrix of shape `(train_count, feature_count)` whose rows are
vector-valued responses for each training element.
Returns
-------
crosswise_dists:
A matrix of shape `(batch_count, nn_count)` whose rows list the distance
of the corresponding batch element to each of its nearest neighbors.
pairwise_dists:
A tensor of shape `(batch_count, nn_count, nn_count,)` whose latter two
dimensions contain square matrices containing the pairwise distances
between the nearest neighbors of the batch elements.
batch_targets:
Matrix of floats of shape `(batch_count, response_count)` whose rows
give the expected response for each batch element.
batch_nn_targets:
Tensor of floats of shape `(batch_count, nn_count, response_count)`
containing the expected response for each nearest neighbor of each batch
element.
"""
crosswise_dists, pairwise_dists, batch_nn_targets = make_regress_tensors(
metric,
batch_indices,
batch_nn_indices,
train_features,
train_features,
train_targets,
)
batch_targets = train_targets[batch_indices, :]
return crosswise_dists, pairwise_dists, batch_targets, batch_nn_targets
[docs]def crosswise_distances(
data: np.ndarray,
nn_data: np.ndarray,
data_indices: np.ndarray,
nn_indices: np.ndarray,
metric: str = "l2",
) -> np.ndarray:
"""
Compute a matrix of distances between data and their nearest neighbors.
Takes full datasets of records of interest `data` and neighbor candidates
`nn_data` and produces the distances between each element of `data`
indicated by `data_indices` and each of the nearest neighbors
in `nn_data` as indicated by the corresponding rows of `nn_indices`. `data`
and `nn_data` can refer to the same dataset.
See the following example computing the crosswise distances between a batch
of training data and their nearest neighbors.
Args:
data:
The data matrix of shape `(data_count, feature_count)` containing
batch elements.
nn_data:
The data matrix of shape `(candidate_count, feature_count)`
containing the universe of candidate neighbors for the batch
elements. Might be the same as `data`.
indices:
An integral vector of shape `(batch_count,)` containing the indices
of the batch.
nn_indices:
An integral matrix of shape (batch_count, nn_count) listing the
nearest neighbor indices for the batch of data points.
metric:
The name of the metric to use in order to form distances. Supported
values are `l2`, `F2`, `ip` (inner product, a distance only if
data is normalized to the unit hypersphere), and `cosine`.
Returns:
A matrix of shape `(batch_count, nn_count)` whose rows list the distance
of the corresponding batch element to each of its nearest neighbors.
"""
locations = data[data_indices]
points = nn_data[nn_indices]
if metric == "l2":
diffs = _crosswise_diffs(locations, points)
return _l2(diffs)
elif metric == "F2":
diffs = _crosswise_diffs(locations, points)
return _F2(diffs)
elif metric == "ip":
return _crosswise_prods(locations, points)
elif metric == "cosine":
return _crosswise_cosine(locations, points)
else:
raise ValueError(f"Metric {metric} is not supported!")
def _crosswise_diffs(locations: np.array, points: np.array) -> np.array:
return locations[:, None, :] - points
def _crosswise_F2(locations: np.array, points: np.array) -> np.array:
return np.array(
[
[
_F2(locations[i, :] - points[i, j, :])
for j in range(points.shape[1])
]
for i in range(points.shape[0])
]
)
def _crosswise_l2(locations: np.array, points: np.array) -> np.array:
return np.array(
[
[
_l2(locations[i, :] - points[i, j, :])
for j in range(points.shape[1])
]
for i in range(points.shape[0])
]
)
def _crosswise_prods(locations: np.array, points: np.array) -> np.array:
return 1 - np.array(
[
[locations[i, :] @ points[i, j, :] for j in range(points.shape[1])]
for i in range(points.shape[0])
]
)
def _crosswise_cosine(locations: np.array, points: np.array) -> np.array:
return 1 - np.array(
[
[
cosine_similarity(locations[i, :], points[i, j, :])
for j in range(points.shape[1])
]
for i in range(points.shape[0])
]
)
[docs]def pairwise_distances(
data: np.ndarray,
nn_indices: np.ndarray,
metric: str = "l2",
) -> np.ndarray:
"""
Compute a tensor of pairwise distances among sets of nearest neighbors.
Takes a full dataset of records of interest `data` and produces the
pairwise distances between the elements indicated by each row of
`nn_indices`.
Args:
data:
The data matrix of shape `(batch_count, feature_count)` containing
batch elements.
nn_indices:
An integral matrix of shape (batch_count, nn_count) listing the
nearest neighbor indices for the batch of data points.
metric:
The name of the metric to use in order to form distances. Supported
values are `l2`, `F2`, `ip` (inner product, a distance only if
data is normalized to the unit hypersphere), and `cosine`.
Returns:
A tensor of shape `(batch_count, nn_count, nn_count,)` whose latter two
dimensions contain square matrices containing the pairwise distances
between the nearest neighbors of the batch elements.
"""
points = data[nn_indices]
if metric == "l2":
diffs = _diffs(points)
return _l2(diffs)
elif metric == "F2":
diffs = _diffs(points)
return _F2(diffs)
elif metric == "ip":
return _prods(points)
elif metric == "cosine":
return _cosine(points)
else:
raise ValueError(f"Metric {metric} is not supported!")
def _diffs(points: np.array) -> np.array:
if len(points.shape) == 3:
return points[:, :, None, :] - points[:, None, :, :]
elif len(points.shape) == 2:
return points[:, None, :] - points[None, :, :]
else:
raise ValueError(f"points shape {points.shape} is not supported.")
def _F2(diffs: np.array) -> np.array:
return np.sum(diffs ** 2, axis=-1)
def _l2(diffs: np.array) -> np.array:
return np.sqrt(_F2(diffs))
def _prods(points: np.array) -> np.array:
if len(points.shape) == 3:
return 1 - np.array([mat @ mat.T for mat in points])
elif len(points.shape) == 2:
return 1 - points @ points.T
else:
raise ValueError(f"points shape {points.shape} is not supported.")
def _cosine(points: np.array) -> np.array:
if len(points.shape) == 3:
return 1 - np.array([cosine_similarity(mat) for mat in points])
elif len(points.shape) == 2:
return 1 - cosine_similarity(points)
else:
raise ValueError(f"points shape {points.shape} is not supported.")