Source code for alf_tools.optimizer.acquisition_functions.core_set
# Copyright 2026 InstaDeep Ltd. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import torch
from alf_core import AcquisitionFunction, Candidate, LabelledCandidates, State
from scipy.spatial.distance import cdist
_CDIST_CHUNK_SIZE = 512
def _to_numpy(features: np.ndarray | torch.Tensor) -> np.ndarray:
"""Convert model features to a numpy array.
Args:
features: Feature output from a model's featurise method.
Returns:
Numpy array representation of the features.
Raises:
ValueError: If features is None or cannot be converted to a numpy array.
"""
if features is None:
raise ValueError("featurise returned None; expected a numpy array or torch.Tensor")
if isinstance(features, torch.Tensor):
return features.detach().cpu().numpy()
try:
return np.asarray(features)
except (TypeError, ValueError) as e:
raise ValueError(
f"featurise returned a value that cannot be converted to a numpy array: {e}"
) from e
[docs]
class CoreSet(AcquisitionFunction):
"""Core-set acquisition function using greedy k-centres.
Greedily selects candidates that maximise the minimum distance to
the training set and previously selected candidates (greedy k-centres).
Candidates are scored by their selection rank (n_select - step), so the
first selected candidate receives the highest score and the last receives 1.
Unselected candidates receive a score of 0.
This is a maximising acquisition function that uses features as a
2-D array of shape (n_inputs, d).
"""
def __call__(
self,
search_candidates: list[Candidate],
state: State,
) -> LabelledCandidates:
"""Compute CoreSet acquisition values for unlabelled candidates.
The model's featurise() must return a 2-D array of shape (n_inputs, d).
Args:
search_candidates: List of unlabelled candidates to score.
state: The task state containing the current datasets and surrogate model.
Raises:
ValueError: If featurise returns None or a non-array-like object.
Returns:
LabelledCandidates with CoreSet acquisition values.
"""
if not search_candidates:
return LabelledCandidates(candidates=[], labels=np.zeros(0))
training_candidates = state.dataset.train_dataset.candidates
features = state.surrogate.featurise(training_candidates + search_candidates)
embeddings = _to_numpy(features)
if embeddings.ndim != 2:
raise ValueError(
"featurise must return a 2-D array of shape (n_inputs, d), "
f"got shape {embeddings.shape}"
)
n_train = len(training_candidates)
n_cands = len(search_candidates)
if len(embeddings) != n_train + n_cands:
raise ValueError(
f"featurise returned {len(embeddings)} rows for "
f"{n_train} training + {n_cands} candidates (expected {n_train + n_cands})"
)
training_embs = embeddings[:n_train]
candidate_embs = embeddings[n_train:]
n_select = min(state.acq_batch_size, n_cands)
if n_train == 0:
min_dists = np.full(n_cands, np.inf)
else:
min_dists = np.empty(n_cands)
for _i in range(0, n_cands, _CDIST_CHUNK_SIZE):
_sl = candidate_embs[_i : _i + _CDIST_CHUNK_SIZE]
min_dists[_i : _i + _CDIST_CHUNK_SIZE] = cdist(_sl, training_embs).min(axis=1)
# Guard against re-selection: np.minimum zeroes selected entries in the
# common case, but if remaining candidates share identical embeddings
# (all pairwise distances = 0), selected_mask prevents argmax from
# re-picking an already-selected index.
selected_mask = np.zeros(n_cands, dtype=bool)
acquisition_values = np.zeros(n_cands)
for step in range(n_select):
masked = np.where(~selected_mask, min_dists, -np.inf)
best_idx = int(np.argmax(masked))
acquisition_values[best_idx] = float(n_select - step)
selected_mask[best_idx] = True
dists_to_new = cdist(candidate_embs, candidate_embs[[best_idx]])[:, 0]
min_dists = np.minimum(min_dists, dists_to_new)
return LabelledCandidates(candidates=search_candidates, labels=acquisition_values)