diff --git a/python/cuml/cuml/manifold/spectral_embedding.pyx b/python/cuml/cuml/manifold/spectral_embedding.pyx index 9c8118ad51..3d2f0cf542 100644 --- a/python/cuml/cuml/manifold/spectral_embedding.pyx +++ b/python/cuml/cuml/manifold/spectral_embedding.pyx @@ -4,13 +4,10 @@ # import cupy as cp import cupyx.scipy.sparse as cp_sp -import numpy as np -import scipy.sparse as sp from cuml.common.array_descriptor import CumlArrayDescriptor from cuml.internals.array import CumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cupy_array from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -60,201 +57,6 @@ cdef extern from "cuml/manifold/spectral_embedding.hpp" \ device_matrix_view[float, int, col_major] embedding) except + -@reflect -def spectral_embedding( - A, - *, - int n_components=8, - affinity="nearest_neighbors", - random_state=None, - n_neighbors=None, - norm_laplacian=True, - drop_first=True, -): - """Project the sample on the first eigenvectors of the graph Laplacian. - - The adjacency matrix is used to compute a normalized graph Laplacian - whose spectrum (especially the eigenvectors associated to the - smallest eigenvalues) has an interpretation in terms of minimal - number of cuts necessary to split the graph into comparably sized - components. - - Note : Laplacian Eigenmaps is the actual algorithm implemented here. - - Parameters - ---------- - A : array-like or sparse matrix of shape (n_samples, n_features) or \ - (n_samples, n_samples) - If affinity is 'nearest_neighbors', this is the input data and a k-NN - graph will be constructed. If affinity is 'precomputed', this is the - affinity matrix. Supported formats for precomputed affinity: scipy - sparse (CSR, CSC, COO), cupy sparse (CSR, CSC, COO), dense numpy - arrays, or dense cupy arrays. - n_components : int, default=8 - The dimension of the projection subspace. - affinity : {'nearest_neighbors', 'precomputed'}, default='nearest_neighbors' - How to construct the affinity matrix. - - 'nearest_neighbors' : construct the affinity matrix by computing a - graph of nearest neighbors. - - 'precomputed' : interpret ``A`` as a precomputed affinity matrix. - random_state : int, RandomState instance or None, default=None - A pseudo random number generator used for the initialization. - Use an int to make the results deterministic across calls. - n_neighbors : int or None, default=None - Number of nearest neighbors for nearest_neighbors graph building. - If None, n_neighbors will be set to max(n_samples/10, 1). - Only used when A has shape (n_samples, n_features). - norm_laplacian : bool, default=True - If True, then compute symmetric normalized Laplacian. - drop_first : bool, default=True - Whether to drop the first eigenvector. For spectral embedding, this - should be True as the first eigenvector should be constant vector for - connected graph, but for spectral clustering, this should be kept as - False to retain the first eigenvector. - - Returns - ------- - embedding : cupy.ndarray of shape (n_samples, n_components) - The reduced samples. - - Notes - ----- - Spectral Embedding (Laplacian Eigenmaps) is most useful when the graph - has one connected component. If there graph has many components, the first - few eigenvectors will simply uncover the connected components of the graph. - - Examples - -------- - >>> import cupy as cp - >>> from cuml.manifold import spectral_embedding - >>> X = cp.random.rand(100, 20, dtype=cp.float32) - >>> embedding = spectral_embedding(X, n_components=2, random_state=42) - >>> embedding.shape - (100, 2) - """ - cdef float* affinity_data_ptr = NULL - cdef int* affinity_rows_ptr = NULL - cdef int* affinity_cols_ptr = NULL - cdef int64_t affinity_nnz = 0 - - if affinity == "nearest_neighbors": - A = input_to_cupy_array( - A, order="C", check_dtype=np.float32, convert_to_dtype=cp.float32 - ).array - - affinity_data_ptr = A.data.ptr - - isfinite = cp.isfinite(A).all() - elif affinity == "precomputed": - # Coerce `A` to a canonical float32 COO sparse matrix - if cp_sp.issparse(A): - A = A.tocoo() - if A.dtype != np.float32: - A = A.astype("float32") - elif sp.issparse(A): - A = cp_sp.coo_matrix(A, dtype="float32") - else: - A = cp_sp.coo_matrix(cp.asarray(A, dtype="float32")) - A.sum_duplicates() - - affinity_data = A.data - affinity_rows = A.row - affinity_cols = A.col - affinity_nnz = A.nnz - - # laplacian kernel expects diagonal to be zero - # remove diagonal elements since they are ignored in laplacian calculation anyway - valid = affinity_rows != affinity_cols - if not valid.all(): - affinity_data = affinity_data[valid] - affinity_rows = affinity_rows[valid] - affinity_cols = affinity_cols[valid] - affinity_nnz = len(affinity_data) - - affinity_data_ptr = affinity_data.data.ptr - affinity_rows_ptr = affinity_rows.data.ptr - affinity_cols_ptr = affinity_cols.data.ptr - - isfinite = cp.isfinite(affinity_data).all() - else: - raise ValueError( - f"`affinity={affinity!r}` is not supported, expected one of " - "['nearest_neighbors', 'precomputed']" - ) - - cdef int n_samples, n_features - n_samples, n_features = A.shape - - if not isfinite: - raise ValueError( - "Input contains NaN or inf; nonfinite values are not supported" - ) - - if n_samples < 2: - raise ValueError( - f"Found array with {n_samples} sample(s) (shape={A.shape}) while a " - f"minimum of 2 is required." - ) - if n_features < 2: - raise ValueError( - f"Found array with {n_features} feature(s) (shape={A.shape}) while " - f"a minimum of 2 is required." - ) - - # Allocate output array - eigenvectors = CumlArray.empty( - (A.shape[0], n_components), dtype=np.float32, order='F' - ) - - cdef params config - cdef uint64_t seed_value - # No seed use nullopt (non-deterministic) or set user seed (deterministic) - if random_state is None: - config.seed = nullopt - else: - seed_value = check_random_seed(random_state) - config.seed = seed_value - config.norm_laplacian = norm_laplacian - config.drop_first = drop_first - config.n_components = n_components + 1 if drop_first else n_components - config.n_neighbors = ( - n_neighbors - if n_neighbors is not None - else max(int(A.shape[0] / 10), 1) - ) - cdef float* eigenvectors_ptr = eigenvectors.ptr - cdef bool precomputed = affinity == "precomputed" - handle = get_handle() - cdef device_resources *handle_ = handle.getHandle() - - with nogil: - if precomputed: - transform( - handle_[0], - config, - make_device_vector_view[int, int64_t](affinity_rows_ptr, affinity_nnz), - make_device_vector_view[int, int64_t](affinity_cols_ptr, affinity_nnz), - make_device_vector_view[float, int64_t](affinity_data_ptr, affinity_nnz), - make_device_matrix_view[float, int, col_major]( - eigenvectors_ptr, n_samples, n_components, - ) - ) - else: - transform( - handle_[0], - config, - make_device_matrix_view[float, int, row_major]( - affinity_data_ptr, n_samples, n_features, - ), - make_device_matrix_view[float, int, col_major]( - eigenvectors_ptr, n_samples, n_components, - ) - ) - handle.sync() - - return eigenvectors - - class SpectralEmbedding(Base, InteropMixin, CMajorInputTagMixin): """Spectral embedding for non-linear dimensionality reduction. @@ -316,6 +118,10 @@ class SpectralEmbedding(Base, InteropMixin, CMajorInputTagMixin): _cpu_class_path = "sklearn.manifold.SpectralEmbedding" embedding_ = CumlArrayDescriptor(order="F") + # Private so that `spectral_embedding` can share the same code + _drop_first = True + _norm_laplacian = True + def __init__( self, n_components=2, @@ -424,23 +230,190 @@ class SpectralEmbedding(Base, InteropMixin, CMajorInputTagMixin): self, X, dtype="float32", - accept_sparse=(self.affinity == "precomputed"), + order="C", + accept_sparse="coo" if self.affinity == "precomputed" else False, + ensure_min_samples=2, + ensure_min_features=2, reset=True, ) - # Store n_neighbors_ for sklearn compatibility + cdef float* affinity_data_ptr = NULL + cdef int* affinity_rows_ptr = NULL + cdef int* affinity_cols_ptr = NULL + cdef int64_t affinity_nnz = 0 + + if self.affinity == "nearest_neighbors": + affinity_data_ptr = X.data.ptr + elif self.affinity == "precomputed": + # Coerce `X` to a canonical float32 COO sparse matrix + if isinstance(X, cp.ndarray): + X = cp_sp.coo_matrix(X) + X.sum_duplicates() + + if X.shape[0] != X.shape[1]: + raise ValueError( + f"Expected precomputed `X` to be square, got shape = {X.shape}" + ) + + affinity_data = X.data + affinity_rows = X.row + affinity_cols = X.col + affinity_nnz = X.nnz + + # laplacian kernel expects diagonal to be zero + # remove diagonal elements since they are ignored in laplacian calculation anyway + valid = affinity_rows != affinity_cols + if not valid.all(): + affinity_data = affinity_data[valid] + affinity_rows = affinity_rows[valid] + affinity_cols = affinity_cols[valid] + affinity_nnz = len(affinity_data) + + affinity_data_ptr = affinity_data.data.ptr + affinity_rows_ptr = affinity_rows.data.ptr + affinity_cols_ptr = affinity_cols.data.ptr + else: + raise ValueError( + f"`affinity={self.affinity!r}` is not supported, expected one of " + "['nearest_neighbors', 'precomputed']" + ) + self.n_neighbors_ = ( self.n_neighbors if self.n_neighbors is not None else max(int(X.shape[0] / 10), 1) ) - self.embedding_ = spectral_embedding( - X, - n_components=self.n_components, - affinity=self.affinity, - random_state=self.random_state, - n_neighbors=self.n_neighbors_, - ) + cdef int n_samples = X.shape[0] + cdef int n_features = X.shape[1] + cdef int n_components = self.n_components + + # Allocate output array + embedding = cp.empty((n_samples, n_components), dtype="float32", order="F") + + cdef params config + # No seed use nullopt (non-deterministic) or set user seed (deterministic) + if self.random_state is None: + config.seed = nullopt + else: + config.seed = check_random_seed(self.random_state) + config.norm_laplacian = self._norm_laplacian + config.drop_first = self._drop_first + config.n_components = n_components + 1 if self._drop_first else n_components + config.n_neighbors = self.n_neighbors_ + cdef float* embedding_ptr = embedding.data.ptr + cdef bool precomputed = self.affinity == "precomputed" + handle = get_handle() + cdef device_resources *handle_ = handle.getHandle() + + with nogil: + if precomputed: + transform( + handle_[0], + config, + make_device_vector_view[int, int64_t](affinity_rows_ptr, affinity_nnz), + make_device_vector_view[int, int64_t](affinity_cols_ptr, affinity_nnz), + make_device_vector_view[float, int64_t](affinity_data_ptr, affinity_nnz), + make_device_matrix_view[float, int, col_major]( + embedding_ptr, n_samples, n_components, + ) + ) + else: + transform( + handle_[0], + config, + make_device_matrix_view[float, int, row_major]( + affinity_data_ptr, n_samples, n_features, + ), + make_device_matrix_view[float, int, col_major]( + embedding_ptr, n_samples, n_components, + ) + ) + handle.sync() + + self.embedding_ = CumlArray(data=embedding) return self + + +@reflect +def spectral_embedding( + A, + *, + int n_components=8, + affinity="nearest_neighbors", + random_state=None, + n_neighbors=None, + norm_laplacian=True, + drop_first=True, +): + """Project the sample on the first eigenvectors of the graph Laplacian. + + The adjacency matrix is used to compute a normalized graph Laplacian + whose spectrum (especially the eigenvectors associated to the + smallest eigenvalues) has an interpretation in terms of minimal + number of cuts necessary to split the graph into comparably sized + components. + + Note : Laplacian Eigenmaps is the actual algorithm implemented here. + + Parameters + ---------- + A : array-like or sparse matrix of shape (n_samples, n_features) or \ + (n_samples, n_samples) + If affinity is 'nearest_neighbors', this is the input data and a k-NN + graph will be constructed. If affinity is 'precomputed', this is the + affinity matrix. Supported formats for precomputed affinity: scipy + sparse (CSR, CSC, COO), cupy sparse (CSR, CSC, COO), dense numpy + arrays, or dense cupy arrays. + n_components : int, default=8 + The dimension of the projection subspace. + affinity : {'nearest_neighbors', 'precomputed'}, default='nearest_neighbors' + How to construct the affinity matrix. + - 'nearest_neighbors' : construct the affinity matrix by computing a + graph of nearest neighbors. + - 'precomputed' : interpret ``A`` as a precomputed affinity matrix. + random_state : int, RandomState instance or None, default=None + A pseudo random number generator used for the initialization. + Use an int to make the results deterministic across calls. + n_neighbors : int or None, default=None + Number of nearest neighbors for nearest_neighbors graph building. + If None, n_neighbors will be set to max(n_samples/10, 1). + Only used when A has shape (n_samples, n_features). + norm_laplacian : bool, default=True + If True, then compute symmetric normalized Laplacian. + drop_first : bool, default=True + Whether to drop the first eigenvector. For spectral embedding, this + should be True as the first eigenvector should be constant vector for + connected graph, but for spectral clustering, this should be kept as + False to retain the first eigenvector. + + Returns + ------- + embedding : cupy.ndarray of shape (n_samples, n_components) + The reduced samples. + + Notes + ----- + Spectral Embedding (Laplacian Eigenmaps) is most useful when the graph + has one connected component. If there graph has many components, the first + few eigenvectors will simply uncover the connected components of the graph. + + Examples + -------- + >>> import cupy as cp + >>> from cuml.manifold import spectral_embedding + >>> X = cp.random.rand(100, 20, dtype=cp.float32) + >>> embedding = spectral_embedding(X, n_components=2, random_state=42) + >>> embedding.shape + (100, 2) + """ + model = SpectralEmbedding( + n_components=n_components, + affinity=affinity, + random_state=random_state, + n_neighbors=n_neighbors, + ) + model._drop_first = drop_first + model._norm_laplacian = norm_laplacian + return model.fit_transform(A) diff --git a/python/cuml/cuml/manifold/umap/umap.pyx b/python/cuml/cuml/manifold/umap/umap.pyx index 85b6385301..9430a2fa79 100644 --- a/python/cuml/cuml/manifold/umap/umap.pyx +++ b/python/cuml/cuml/manifold/umap/umap.pyx @@ -21,7 +21,7 @@ from cuml.internals import logger, reflect from cuml.internals.array import CumlArray from cuml.internals.array_sparse import SparseCumlArray from cuml.internals.base import Base, get_handle -from cuml.internals.input_utils import input_to_cuml_array, is_array_like +from cuml.internals.input_utils import is_array_like from cuml.internals.interop import ( InteropMixin, UnsupportedOnGPU, @@ -31,9 +31,11 @@ from cuml.internals.interop import ( from cuml.internals.mixins import CMajorInputTagMixin, SparseInputTagMixin from cuml.internals.validation import ( check_array, + check_consistent_length, check_inputs, check_is_fitted, check_random_seed, + check_y, ) from libc.stdint cimport int64_t, uintptr_t @@ -1008,7 +1010,9 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): def _attrs_from_cpu(self, model): if scipy.sparse.issparse(model._raw_data): - raw_data = SparseCumlArray(model._raw_data, convert_to_dtype=cp.float32) + raw_data = SparseCumlArray( + check_array(model._raw_data, dtype="float32", accept_sparse="csr") + ) else: raw_data = to_gpu(model._raw_data) @@ -1177,55 +1181,62 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): memory usage, the precomputed knn graph should be CPU-accessible arrays such as numpy arrays. """ - if len(X.shape) != 2: - raise ValueError("Reshape your data: data should be two dimensional") + # Normalize X as cheaply as possible to minimize copies and work + X, index = check_inputs( + self, + X, + order=None, + mem_type=None, + accept_sparse=True, + ensure_all_finite=False, + return_index=True, + reset=True, + ) + if y is not None: + y = check_y( + y, + dtype="float32", + convert_dtype=convert_dtype, + order="C", + ) + check_consistent_length(X, y) cdef int n_rows = X.shape[0] cdef int n_dims = X.shape[1] - cdef bool X_is_sparse = is_sparse(X) cdef lib.UMAPParams params init_params(self, params, n_rows=n_rows, is_sparse=X_is_sparse) - # Don't coerce to device memory for dense case when using a precomputed - # KNN, so that X may be dropped earlier if passed on host. - if knn_graph is None and self.precomputed_knn is None: - base_mem_type = "device" - else: - base_mem_type = None - + # Determine the required mem_type based on params and X if X_is_sparse: - mem_type = base_mem_type + mem_type = "device" + elif params.build_algo == lib.graph_build_algo.NN_DESCENT: + mem_type = "host" elif ( - params.build_algo == lib.graph_build_algo.NN_DESCENT - or ( - params.build_algo == lib.graph_build_algo.BRUTE_FORCE_KNN - and params.build_params.n_clusters > 1 - ) + params.build_algo == lib.graph_build_algo.BRUTE_FORCE_KNN + and params.build_params.n_clusters > 1 ): mem_type = "host" + elif knn_graph is not None or self.precomputed_knn is not None: + # For dense inputs using a precomputed KNN, we leave the input in + # its original mem_type so the device memory may be dropped earlier + # if passed on host. + mem_type = None else: - mem_type = base_mem_type + mem_type = "device" - check_kwargs = dict( + # Now fully validate and coerce X to the required mem_type + X = check_array( + X, + mem_type=mem_type, dtype="float32", - y_dtype="float32", convert_dtype=convert_dtype, order="C", accept_sparse="csr", - mem_type=mem_type, - reset=True, - return_index=True, ensure_min_samples=2, + input_name="X", ) - if y is not None: - X, y, index = check_inputs(self, X, y, **check_kwargs) - # `y` needs to be on GPU but `check_inputs` doesn't have separate - # mem_type handling for X and y. - y = cp.asarray(y) - else: - X, index = check_inputs(self, X, **check_kwargs) cdef uintptr_t X_ptr = 0, X_indices_ptr = 0, X_indptr_ptr = 0 cdef size_t X_nnz = 0 @@ -1278,20 +1289,24 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): handle_ = handle.getHandle() if is_array_like(self.init): - init_m = input_to_cuml_array( + init = check_array( self.init, + dtype="float32", order="C", - check_dtype=np.float32, - convert_to_dtype=np.float32, - convert_to_mem_type=False, - check_rows=n_rows, - check_cols=self.n_components, - ).array - + mem_type=None, + input_name="init", + ) + if init.shape != (n_rows, self.n_components): + raise ValueError( + f"Expected `init` with shape {(n_rows, self.n_components)}, " + f"got {init.shape}" + ) embeddings_buffer.reset( new device_buffer( - init_m.ptr, - init_m.size, + ( + init.data.ptr if isinstance(init, cp.ndarray) else init.ctypes.data + ), + init.nbytes, handle_.get_stream(), make_any_device_resource(get_current_device_resource().get_mr()) ) @@ -1654,7 +1669,7 @@ class UMAP(Base, InteropMixin, CMajorInputTagMixin, SparseInputTagMixin): ) handle.sync() - return CumlArray(data=inv_transformed_gpu, order="C", index=index) + return CumlArray(data=inv_transformed_gpu, index=index) def fuzzy_simplicial_set( @@ -1731,15 +1746,10 @@ def fuzzy_simplicial_set( of the matrix represents the membership strength of the 1-simplex between the ith and jth sample points. """ - X_m = input_to_cuml_array( - X, - order="C", - check_dtype=np.float32, - convert_to_dtype=np.float32 - ).array + X = check_array(X, order="C", dtype="float32", input_name="X") - cdef int n_rows = X_m.shape[0] - cdef int n_cols = X_m.shape[1] + cdef int n_rows = X.shape[0] + cdef int n_cols = X.shape[1] cdef lib.UMAPParams params params.n_neighbors = n_neighbors @@ -1753,24 +1763,23 @@ def fuzzy_simplicial_set( cdef uintptr_t X_ptr, knn_indices_ptr, knn_dists_ptr if knn_indices is not None and knn_dists is not None: - knn_indices_m = input_to_cuml_array( + knn_indices = check_array( knn_indices, + dtype="int64", order="C", - check_dtype=np.int64, - convert_to_dtype=np.int64 - ).array - knn_dists_m = input_to_cuml_array( + input_name="knn_indices", + ) + knn_dists = check_array( knn_dists, + dtype="float32", order="C", - check_dtype=np.float32, - convert_to_dtype=np.float32 - ).array - + input_name="knn_dists", + ) X_ptr = 0 - knn_indices_ptr = knn_indices_m.ptr - knn_dists_ptr = knn_dists_m.ptr + knn_indices_ptr = knn_indices.data.ptr + knn_dists_ptr = knn_dists.data.ptr else: - X_ptr = X_m.ptr + X_ptr = X.data.ptr knn_indices_ptr = 0 knn_dists_ptr = 0 @@ -1884,12 +1893,14 @@ def simplicial_set_embedding( The optimized of ``graph`` into an ``n_components`` dimensional euclidean space. """ - X = input_to_cuml_array( + X, index = check_array( data, + dtype="float32", + convert_dtype=convert_dtype, order="C", - convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=np.float32, - ).array + input_name="X", + return_index=True, + ) cdef int n_rows = X.shape[0] cdef int n_cols = X.shape[1] @@ -1924,19 +1935,21 @@ def simplicial_set_embedding( cdef bool initialized = is_array_like(init) if initialized: - embedding = input_to_cuml_array( + embedding = check_array( init, + dtype="float32", + convert_dtype=convert_dtype, order="C", - convert_to_dtype=(np.float32 if convert_dtype else None), - check_dtype=np.float32, - check_rows=n_rows, - check_cols=n_components, - ).array + input_name="init", + ) + if embedding.shape != (n_rows, n_components): + raise ValueError( + f"Expected `init` with shape {(n_rows, n_components)}, " + f"got {embedding.shape}" + ) elif isinstance(init, str) and init in _INITS: params.init = _INITS[init] - embedding = CumlArray.zeros( - (n_rows, n_components), order="C", dtype=np.float32, index=X.index, - ) + embedding = cp.zeros((n_rows, n_components), order="C", dtype="float32") else: raise ValueError( "Expected `init` to be an array or one of ['random', 'spectral'], " @@ -1951,8 +1964,8 @@ def simplicial_set_embedding( handle = get_handle() cdef handle_t* handle_ = handle.getHandle() cdef RaftCOO fss_graph = RaftCOO.from_cupy_coo(handle, graph) - cdef uintptr_t embedding_ptr = embedding.ptr - cdef uintptr_t X_ptr = X.ptr + cdef uintptr_t embedding_ptr = embedding.data.ptr + cdef uintptr_t X_ptr = X.data.ptr if initialized: lib.refine( @@ -1974,4 +1987,4 @@ def simplicial_set_embedding( ¶ms, embedding_ptr ) - return embedding + return CumlArray(data=embedding, index=index) diff --git a/python/cuml/cuml/naive_bayes/naive_bayes.py b/python/cuml/cuml/naive_bayes/naive_bayes.py index b8703321b9..3b85104f93 100644 --- a/python/cuml/cuml/naive_bayes/naive_bayes.py +++ b/python/cuml/cuml/naive_bayes/naive_bayes.py @@ -14,7 +14,6 @@ from cuml.common.classification import decode_labels from cuml.common.doc_utils import generate_docstring from cuml.internals.base import Base -from cuml.internals.input_utils import input_to_cupy_array from cuml.internals.mixins import ClassifierMixin, SparseInputTagMixin from cuml.internals.outputs import ( exit_internal_context, @@ -346,18 +345,18 @@ def _partial_fit( self.class_count_ = cp.zeros(n_classes, dtype=X.dtype) if self.priors is not None: - if len(self.priors) != n_classes: + priors = check_array( + self.priors, dtype=X.dtype, ensure_2d=False + ) + if priors.shape[0] != n_classes: raise ValueError( "Number of priors must match number of classes." ) - if not cp.isclose(self.priors.sum(), 1): + if not cp.isclose(priors.sum(), 1.0): raise ValueError("The sum of the priors should be 1.") - if (self.priors < 0).any(): + if (priors < 0).any(): raise ValueError("Priors must be non-negative.") - self.class_prior_ = input_to_cupy_array( - self.priors, check_dtype=[cp.float32, cp.float64] - ).array - + self.class_prior_ = priors else: self.sigma_[:, :] -= self.epsilon_ diff --git a/python/cuml/tests/test_sklearn_compatibility.py b/python/cuml/tests/test_sklearn_compatibility.py index 939784ec22..b4a6dbc4f7 100644 --- a/python/cuml/tests/test_sklearn_compatibility.py +++ b/python/cuml/tests/test_sklearn_compatibility.py @@ -2,7 +2,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # -from functools import partial import pytest from sklearn.utils import estimator_checks @@ -62,7 +61,7 @@ CategoricalNB(), BernoulliNB(), MultinomialNB(), - UMAP(), + UMAP(n_neighbors=5), TSNE(), TruncatedSVD(), IncrementalPCA(), @@ -299,14 +298,6 @@ ) -def _check_name(check): - if hasattr(check, "__wrapped__"): - return _check_name(check.__wrapped__) - return ( - check.func.__name__ if isinstance(check, partial) else check.__name__ - ) - - @estimator_checks.parametrize_with_checks( ESTIMATORS, expected_failed_checks=lambda est: XFAILS.get(type(est), {}), @@ -328,13 +319,4 @@ def _check_name(check): @pytest.mark.filterwarnings("ignore:The number of bins.*:UserWarning") @pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") def test_sklearn_compatible_estimator(estimator, check): - # Check that all estimators pass the "common estimator" checks - # provided by scikit-learn - check_name = _check_name(check) - - if check_name in ["check_estimators_nan_inf"] and isinstance( - estimator, UMAP - ): - pytest.skip("UMAP does not handle Nans and infinities") - check(estimator) diff --git a/python/cuml/tests/test_spectral_embedding.py b/python/cuml/tests/test_spectral_embedding.py index 1d9c11f04b..c0ffda357f 100644 --- a/python/cuml/tests/test_spectral_embedding.py +++ b/python/cuml/tests/test_spectral_embedding.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # @@ -194,7 +194,8 @@ def test_spectral_embedding_invalid_affinity(): def test_spectral_embedding_nonfinite(value, affinity): X = np.array([[0, 1], [2, 3], [0, value]], dtype="float32") - with pytest.raises(ValueError, match="nonfinite"): + msg = "contains NaN" if np.isnan(value) else "contains infinity" + with pytest.raises(ValueError, match=msg): spectral_embedding(X, affinity=affinity) @@ -320,3 +321,12 @@ def test_precomputed_no_sparsity(): ) out = embedding_precomp.fit_transform(affinity_matrix) assert out.shape == (200, 2) + + +def test_precomputed_not_square(): + model = SpectralEmbedding(affinity="precomputed") + X = np.random.default_rng(42).random((20, 25)) + with pytest.raises( + ValueError, match="Expected precomputed `X` to be square" + ): + model.fit(X) diff --git a/python/cuml/tests/test_umap.py b/python/cuml/tests/test_umap.py index 6891c803b4..4359e0132b 100644 --- a/python/cuml/tests/test_umap.py +++ b/python/cuml/tests/test_umap.py @@ -1154,13 +1154,13 @@ def test_umap_custom_init_errors(): # Wrong number of samples init_wrong_samples = np.zeros((n_samples + 1, 2), dtype=np.float32) model = cuUMAP(init=init_wrong_samples) - with pytest.raises(ValueError, match=".*rows.*"): + with pytest.raises(ValueError, match="Expected `init` with shape"): model.fit(data) # Wrong number of components init_wrong_components = np.zeros((n_samples, 3), dtype=np.float32) model = cuUMAP(init=init_wrong_components, n_components=2) - with pytest.raises(ValueError, match=".*columns.*"): + with pytest.raises(ValueError, match="Expected `init` with shape"): model.fit(data)