Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions python/cuml/cuml/metrics/cluster/adjusted_rand_index.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#
import cupy as cp
import numpy as np

from cuml.common import input_to_cuml_array
from cuml.internals import get_handle
from cuml.internals.validation import check_array, check_consistent_length

from libc.stdint cimport uintptr_t
from pylibraft.common.handle cimport handle_t
Expand Down Expand Up @@ -38,19 +38,35 @@ def adjusted_rand_score(labels_true, labels_pred, convert_dtype=True) -> float:
handle = get_handle()
cdef handle_t* handle_ = <handle_t*><size_t>handle.getHandle()

labels_true, n_rows, _, _ = \
input_to_cuml_array(labels_true, order='C', check_dtype=cp.int32,
convert_to_dtype=(cp.int32 if convert_dtype
else None))

labels_pred, _, _, _ = \
input_to_cuml_array(labels_pred, order='C', check_dtype=cp.int32,
convert_to_dtype=(cp.int32 if convert_dtype
else None))
labels_true = check_array(
labels_true,
ensure_2d=False,
ensure_min_samples=0,
order='C',
dtype=np.int32,
convert_dtype=convert_dtype,
input_name='labels_true',
)
labels_pred = check_array(
labels_pred,
ensure_2d=False,
ensure_min_samples=0,
order='C',
dtype=np.int32,
convert_dtype=convert_dtype,
input_name='labels_pred',
)
if labels_true.ndim != 1 or labels_pred.ndim != 1:
raise ValueError(
"labels_true and labels_pred must be 1D arrays, got shapes "
f"{labels_true.shape} and {labels_pred.shape}"
)
check_consistent_length(labels_true, labels_pred)
cdef int n_rows = labels_true.shape[0]

rand_score = adjusted_rand_index(handle_[0],
<int*><uintptr_t> labels_true.ptr,
<int*><uintptr_t> labels_pred.ptr,
<int> n_rows)
<int*><uintptr_t> labels_true.data.ptr,
<int*><uintptr_t> labels_pred.data.ptr,
n_rows)

return rand_score
4 changes: 2 additions & 2 deletions python/cuml/cuml/metrics/cluster/completeness_score.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def cython_completeness_score(labels_true, labels_pred) -> float:
labels_pred
)

cdef uintptr_t ground_truth_ptr = y_true.ptr
cdef uintptr_t preds_ptr = y_pred.ptr
cdef uintptr_t ground_truth_ptr = y_true.data.ptr
cdef uintptr_t preds_ptr = y_pred.data.ptr

com = completeness_score(handle_[0],
<int*> ground_truth_ptr,
Expand Down
19 changes: 14 additions & 5 deletions python/cuml/cuml/metrics/cluster/entropy.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cupy as cp
import numpy as np

from cuml.internals import get_handle
from cuml.internals.input_utils import input_to_cupy_array
from cuml.internals.validation import check_array

from libc.stdint cimport uintptr_t
from pylibraft.common.handle cimport handle_t
Expand Down Expand Up @@ -44,19 +44,28 @@ def cython_entropy(clustering, base=None) -> float:
handle = get_handle()
cdef handle_t *handle_ = <handle_t*> <size_t> handle.getHandle()

clustering, n_rows, _, _ = input_to_cupy_array(
clustering = check_array(
clustering,
check_dtype=np.int32,
check_cols=1
ensure_2d=False,
order='C',
dtype=np.int32,
input_name='clustering',
)
if clustering.ndim == 2 and clustering.shape[1] != 1:
raise ValueError(
"clustering must have shape (n_samples,) or (n_samples, 1), got "
f"{clustering.shape}"
)
clustering = clustering.ravel()
cdef int n_rows = clustering.shape[0]
lower_class_range = cp.min(clustering).item()
upper_class_range = cp.max(clustering).item()

cdef uintptr_t clustering_ptr = clustering.data.ptr

S = entropy(handle_[0],
<int*> clustering_ptr,
<int> n_rows,
n_rows,
<int> lower_class_range,
<int> upper_class_range)

Expand Down
4 changes: 2 additions & 2 deletions python/cuml/cuml/metrics/cluster/homogeneity_score.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def cython_homogeneity_score(labels_true, labels_pred) -> float:
labels_pred
)

cdef uintptr_t ground_truth_ptr = y_true.ptr
cdef uintptr_t preds_ptr = y_pred.ptr
cdef uintptr_t ground_truth_ptr = y_true.data.ptr
cdef uintptr_t preds_ptr = y_pred.data.ptr

hom = homogeneity_score(handle_[0],
<int*> ground_truth_ptr,
Expand Down
4 changes: 2 additions & 2 deletions python/cuml/cuml/metrics/cluster/mutual_info_score.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ def cython_mutual_info_score(labels_true, labels_pred) -> float:
labels_pred
)

cdef uintptr_t ground_truth_ptr = y_true.ptr
cdef uintptr_t preds_ptr = y_pred.ptr
cdef uintptr_t ground_truth_ptr = y_true.data.ptr
cdef uintptr_t preds_ptr = y_pred.data.ptr

mi = mutual_info_score(handle_[0],
<int*> ground_truth_ptr,
Expand Down
73 changes: 40 additions & 33 deletions python/cuml/cuml/metrics/cluster/silhouette_score.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import cupy as cp
import numpy as np

from cuml.common import input_to_cuml_array
from cuml.internals import get_handle
from cuml.internals.validation import check_array, check_consistent_length
from cuml.metrics.pairwise_distances import _determine_metric

from libc.stdint cimport uintptr_t
Expand Down Expand Up @@ -72,62 +72,69 @@ def _silhouette_coeff(
if chunksize is None:
chunksize = 40000

data, n_rows, n_cols, dtype = input_to_cuml_array(
data = check_array(
X,
order='C',
convert_to_dtype=(np.float32 if convert_dtype
else None),
check_dtype=[np.float32, np.float64],
dtype=[np.float32, np.float64],
convert_dtype=convert_dtype,
input_name='X',
)
cdef int n_rows = data.shape[0]
cdef int n_cols = data.shape[1]
dtype = data.dtype

labels, _, _, _ = input_to_cuml_array(
labels = check_array(
labels,
ensure_2d=False,
order='C',
convert_to_dtype=np.int32
dtype=np.int32,
input_name='labels',
)
if labels.ndim != 1:
raise ValueError(
f"labels must be a 1D array, got shape {labels.shape}"
)
check_consistent_length(data, labels)

# Use cp.unique with return_inverse to get monotonic labels efficiently
labels_cupy = labels.to_output(output_type='cupy', output_dtype='int')
unique_labels, inverse = cp.unique(labels_cupy, return_inverse=True)
n_labels = unique_labels.shape[0]

mono_labels, _, _, _ = input_to_cuml_array(
inverse,
order='C',
convert_to_dtype=np.int32
)
# Use cp.unique with return_inverse to get monotonic labels efficiently.
unique_labels, inverse = cp.unique(labels, return_inverse=True)
cdef int n_labels = unique_labels.shape[0]
mono_labels = cp.ascontiguousarray(inverse, dtype=np.int32)

cdef uintptr_t scores_ptr
if sil_scores is None:
scores_ptr = <uintptr_t> NULL
else:
sil_scores = input_to_cuml_array(
sil_scores = check_array(
sil_scores,
convert_to_dtype=(dtype if convert_dtype
else None),
check_dtype=dtype)[0]

scores_ptr = sil_scores.ptr
ensure_2d=False,
order='C',
dtype=[dtype],
convert_dtype=convert_dtype,
input_name='sil_scores',
ensure_all_finite=False, # output buffer may be uninitialized
)
scores_ptr = sil_scores.data.ptr

metric = _determine_metric(metric)

if dtype == np.float32:
return silhouette_score(handle_[0],
<float*> <uintptr_t> data.ptr,
<int> n_rows,
<int> n_cols,
<int*> <uintptr_t> mono_labels.ptr,
<int> n_labels,
<float*> <uintptr_t> data.data.ptr,
n_rows,
n_cols,
<int*> <uintptr_t> mono_labels.data.ptr,
n_labels,
<float*> scores_ptr,
<int> chunksize,
<DistanceType> metric)
elif dtype == np.float64:
return silhouette_score(handle_[0],
<double*> <uintptr_t> data.ptr,
<int> n_rows,
<int> n_cols,
<int*> <uintptr_t> mono_labels.ptr,
<int> n_labels,
<double*> <uintptr_t> data.data.ptr,
n_rows,
n_cols,
<int*> <uintptr_t> mono_labels.data.ptr,
n_labels,
<double*> scores_ptr,
<int> chunksize,
<DistanceType> metric)
Expand Down
47 changes: 32 additions & 15 deletions python/cuml/cuml/metrics/cluster/utils.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION.
# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#
import cupy as cp
import numpy as np

from cuml.common import input_to_cuml_array
from cuml.internals.validation import check_array, check_consistent_length
from cuml.metrics.utils import sorted_unique_labels
from cuml.prims.label import make_monotonic


def prepare_cluster_metric_inputs(labels_true, labels_pred):
"""Helper function to avoid code duplication for homogeneity score, mutual
info score and completeness score.

Returns ``(y_true, y_pred, n_rows, lower_class_range, upper_class_range)``
where ``y_true`` and ``y_pred`` are C-contiguous int32 ``cupy.ndarray``
arrays whose label values have been remapped to the contiguous range
``[0, len(classes) - 1]``.
"""
y_true, n_rows, _, dtype = input_to_cuml_array(
y_true = check_array(
labels_true,
check_dtype=[cp.int32, cp.int64],
check_cols=1,
deepcopy=True, # deepcopy because we call make_monotonic inplace below
ensure_2d=False,
ensure_min_samples=0,
order="C",
dtype=np.int32,
input_name="labels_true",
)

y_pred, _, _, _ = input_to_cuml_array(
y_pred = check_array(
labels_pred,
check_dtype=dtype,
check_rows=n_rows,
check_cols=1,
deepcopy=True, # deepcopy because we call make_monotonic inplace below
ensure_2d=False,
ensure_min_samples=0,
order="C",
dtype=np.int32,
input_name="labels_pred",
)
if y_true.ndim != 1 or y_pred.ndim != 1:
raise ValueError(
"labels_true and labels_pred must be 1D arrays, got shapes "
f"{y_true.shape} and {y_pred.shape}"
)
check_consistent_length(y_true, y_pred)
n_rows = y_true.shape[0]

classes = sorted_unique_labels(y_true, y_pred)

make_monotonic(y_true, classes=classes, copy=False)
make_monotonic(y_pred, classes=classes, copy=False)
# Make copies so that we never mutate the caller's input arrays.
# ``make_monotonic`` with ``copy=True`` returns new cupy arrays; we
# use those for the downstream Cython callers.
y_true, _ = make_monotonic(y_true, classes=classes, copy=True)
y_pred, _ = make_monotonic(y_pred, classes=classes, copy=True)

# Those values are only correct because we used make_monotonic
lower_class_range = 0
Expand Down
4 changes: 2 additions & 2 deletions python/cuml/cuml/metrics/cluster/v_measure.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ def cython_v_measure(labels_true, labels_pred, beta=1.0) -> float:
labels_pred
)

cdef uintptr_t ground_truth_ptr = y_true.ptr
cdef uintptr_t preds_ptr = y_pred.ptr
cdef uintptr_t ground_truth_ptr = y_true.data.ptr
cdef uintptr_t preds_ptr = y_pred.data.ptr

v_measure_value = v_measure(handle_[0],
<int*> ground_truth_ptr,
Expand Down
Loading