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
79 changes: 29 additions & 50 deletions python/cuml/cuml/covariance/ledoit_wolf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
# SPDX-License-Identifier: Apache-2.0
#

import warnings

import cupy as cp
Expand All @@ -12,9 +11,8 @@
from cuml.internals import reflect, run_in_internal_context
from cuml.internals.array import CumlArray
from cuml.internals.base import Base
from cuml.internals.input_utils import input_to_cupy_array
from cuml.internals.interop import InteropMixin, to_cpu, to_gpu
from cuml.internals.validation import check_features, check_is_fitted
from cuml.internals.validation import check_inputs, check_is_fitted


def _ledoit_wolf_shrinkage(X, assume_centered=False, block_size=1000):
Expand Down Expand Up @@ -228,7 +226,7 @@ def __init__(
self.assume_centered = assume_centered
self.block_size = block_size

@reflect(reset=True)
@reflect(reset="type")
def fit(self, X, y=None, *, convert_dtype=True) -> "LedoitWolf":
"""Fit the Ledoit-Wolf shrunk covariance model to X.

Expand All @@ -247,38 +245,32 @@ def fit(self, X, y=None, *, convert_dtype=True) -> "LedoitWolf":
self : LedoitWolf
Returns the instance itself.
"""
X_arr, n_samples, n_features, dtype = input_to_cupy_array(
X = check_inputs(
self,
X,
check_dtype=[np.float32, np.float64],
order="C",
convert_to_dtype=(np.float32 if convert_dtype else None),
dtype=("float32", "float64"),
convert_dtype=convert_dtype,
reset=True,
)

if n_samples == 0:
raise ValueError(
f"Found array with 0 sample(s) (shape={tuple(X_arr.shape)}) while a "
"minimum of 1 is required."
)

if n_samples == 1:
if X.shape[0] == 1:
warnings.warn(
"Only one sample available. "
"You may want to reshape your data array"
)
Comment thread
jcrist marked this conversation as resolved.

if self.assume_centered:
location = cp.zeros(n_features, dtype=dtype)
location = cp.zeros(X.shape[1], dtype=X.dtype)
else:
location = cp.mean(X_arr, axis=0)
location = cp.mean(X, axis=0)

shrinkage, emp_cov, mu = _ledoit_wolf_shrinkage(
X_arr,
X,
assume_centered=self.assume_centered,
block_size=self.block_size,
)

shrunk_cov = (1.0 - shrinkage) * emp_cov
shrunk_cov.flat[:: n_features + 1] += shrinkage * mu
shrunk_cov.flat[:: X.shape[1] + 1] += shrinkage * mu

self.shrinkage_ = shrinkage
self.location_ = CumlArray(data=location)
Expand All @@ -305,7 +297,7 @@ def get_precision(self):
if self.store_precision:
return self.precision_
else:
covariance = cp.asarray(self.covariance_)
covariance = self.covariance_.to_output("cupy")
precision = cp.linalg.pinv(covariance)
return precision

Expand All @@ -328,24 +320,22 @@ def score(self, X_test, y=None) -> float:
Log-likelihood of the data under the fitted Gaussian model.
"""
check_is_fitted(self)
check_features(self, X_test)

X_arr, _, n_features, _ = input_to_cupy_array(
X_test = check_inputs(
self,
X_test,
check_dtype=[np.float32, np.float64],
check_cols=self.n_features_in_,
order="C",
dtype=("float32", "float64"),
)
precision = self.get_precision().to_output("cupy")
location = self.location_.to_output("cupy")

precision = cp.asarray(self.get_precision())
location = cp.asarray(self.location_)

X_centered = X_arr - location
X_centered = X_test - location
log_det_precision = cp.linalg.slogdet(precision)[1]
mahal = cp.sum(cp.dot(X_centered, precision) * X_centered, axis=1)

log_likelihood = -0.5 * (
n_features * np.log(2 * np.pi) - log_det_precision + cp.mean(mahal)
X_test.shape[1] * np.log(2 * np.pi)
- log_det_precision
+ cp.mean(mahal)
)

return float(log_likelihood)
Expand Down Expand Up @@ -375,14 +365,10 @@ def error_norm(
"""
check_is_fitted(self)

comp_cov_arr, _, _, _ = input_to_cupy_array(
comp_cov,
check_dtype=[np.float32, np.float64],
order="C",
)
self_cov = cp.asarray(self.covariance_)
comp_cov = check_inputs(self, comp_cov, dtype=("float32", "float64"))
self_cov = self.covariance_.to_output("cupy")

diff = self_cov - comp_cov_arr
diff = self_cov - comp_cov

if norm == "frobenius":
error = cp.sum(diff**2)
Expand Down Expand Up @@ -417,18 +403,11 @@ def mahalanobis(self, X):
Squared Mahalanobis distances of the observations.
"""
check_is_fitted(self)
check_features(self, X)

X_arr, _, _, _ = input_to_cupy_array(
X,
check_dtype=[np.float32, np.float64],
check_cols=self.n_features_in_,
order="C",
)
precision = cp.asarray(self.get_precision())
location = cp.asarray(self.location_)
X = check_inputs(self, X, dtype=("float32", "float64"))
precision = self.get_precision().to_output("cupy")
location = self.location_.to_output("cupy")

X_centered = X_arr - location
X_centered = X - location
mahal = cp.sum(cp.dot(X_centered, precision) * X_centered, axis=1)

return mahal
Original file line number Diff line number Diff line change
Expand Up @@ -1023,9 +1023,6 @@
- "sklearn.tests.test_common::test_estimators[KernelDensity()-check_dtype_object]"
- "sklearn.tests.test_common::test_estimators[KernelDensity()-check_estimators_nan_inf]"
- "sklearn.tests.test_common::test_estimators[KernelDensity()-check_sample_weights_not_an_array]"
- "sklearn.tests.test_common::test_estimators[LedoitWolf()-check_dtype_object]"
- "sklearn.tests.test_common::test_estimators[LedoitWolf()-check_estimators_empty_data_messages]"
- "sklearn.tests.test_common::test_estimators[LedoitWolf()-check_estimators_nan_inf]"
- "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_dtype_object]"
- "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_estimators_empty_data_messages]"
- "sklearn.tests.test_common::test_estimators[NearestNeighbors()-check_estimators_nan_inf]"
Expand Down
3 changes: 0 additions & 3 deletions python/cuml/tests/test_sklearn_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,6 @@
},
LedoitWolf: {
"check_estimator_tags_renamed": "No support for modern tags infrastructure",
"check_dtype_object": "LedoitWolf does not handle object dtype",
"check_estimators_empty_data_messages": "LedoitWolf does not handle empty data",
"check_estimators_nan_inf": "LedoitWolf does not check for NaN and inf",
},
DBSCAN: {
"check_estimator_tags_renamed": "No support for modern tags infrastructure",
Expand Down
Loading