diff --git a/python/cuml/cuml/multiclass/multiclass.py b/python/cuml/cuml/multiclass/multiclass.py index d09f43dd7b..565a720253 100644 --- a/python/cuml/cuml/multiclass/multiclass.py +++ b/python/cuml/cuml/multiclass/multiclass.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # +import warnings import cuml.internals from cuml.common import ( @@ -13,70 +14,8 @@ from cuml.internals.mixins import ClassifierMixin -class MulticlassClassifier(Base, ClassifierMixin): - """ - Wrapper around scikit-learn multiclass classifiers that allows to - choose different multiclass strategies. - - The input can be any kind of cuML compatible array, and the output type - follows cuML's output type configuration rules. - - Berofe passing the data to scikit-learn, it is converted to host (numpy) - array. Under the hood the data is partitioned for binary classification, - and it is transformed back to the device by the cuML estimator. These - copies back and forth the device and the host have some overhead. For more - details see issue https://github.com/rapidsai/cuml/issues/2876. - - Examples - -------- - - .. code-block:: python - - >>> from cuml.linear_model import LogisticRegression - >>> from cuml.multiclass import MulticlassClassifier - >>> from cuml.datasets.classification import make_classification - - >>> X, y = make_classification(n_samples=10, n_features=6, - ... n_informative=4, n_classes=3, - ... random_state=137) - - >>> cls = MulticlassClassifier(LogisticRegression(), strategy='ovo') - >>> cls.fit(X, y) - MulticlassClassifier(estimator=LogisticRegression()) - >>> cls.predict(X) - array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) - - Parameters - ---------- - estimator : cuML estimator - handle : cuml.Handle - Specifies the cuml.handle that holds internal CUDA state for - computations in this model. Most importantly, this specifies the CUDA - stream that will be used for the model's computations, so users can - run different models concurrently in different streams by creating - handles in several streams. - If it is None, a new one is created. - verbose : int or boolean, default=False - Sets logging level. It must be one of `cuml.common.logger.level_*`. - See :ref:`verbosity-levels` for more info. - output_type : {'input', 'array', 'dataframe', 'series', 'df_obj', \ - 'numba', 'cupy', 'numpy', 'cudf', 'pandas'}, default=None - Return results and set estimator attributes to the indicated output - type. If None, the output type set at the module level - (`cuml.global_settings.output_type`) will be used. See - :ref:`output-data-type-configuration` for more info. - strategy: string {'ovr', 'ovo'}, default='ovr' - Multiclass classification strategy: 'ovr': one vs. rest or 'ovo': one - vs. one - - Attributes - ---------- - classes_ : float, shape (`n_classes_`) - Array of class labels. - n_classes_ : int - Number of classes. - - """ +class _BaseMulticlassClassifier(Base, ClassifierMixin): + """Shared base class for multiclass classifiers""" def __init__( self, @@ -85,31 +24,15 @@ def __init__( handle=None, verbose=False, output_type=None, - strategy="ovr", ): super().__init__( handle=handle, verbose=verbose, output_type=output_type ) - self.strategy = strategy self.estimator = estimator - import sklearn.multiclass - - if self.strategy == "ovr": - self.multiclass_estimator = sklearn.multiclass.OneVsRestClassifier( - self.estimator, n_jobs=None - ) - elif self.strategy == "ovo": - self.multiclass_estimator = sklearn.multiclass.OneVsOneClassifier( - self.estimator, n_jobs=None - ) - else: - raise ValueError( - "Invalid multiclass strategy " - + str(self.strategy) - + ", must be one of " - '{"ovr", "ovo"}' - ) + @classmethod + def _get_param_names(cls): + return [*super()._get_param_names(), "estimator"] @property @cuml.internals.api_base_return_array_skipall @@ -117,16 +40,28 @@ def classes_(self): return self.multiclass_estimator.classes_ @generate_docstring(y="dense_anydtype") - def fit(self, X, y) -> "MulticlassClassifier": + def fit(self, X, y) -> "_BaseMulticlassClassifier": """ Fit a multiclass classifier. """ - X = input_to_host_array_with_sparse_support(X) + import sklearn.multiclass + opts = { + "ovo": sklearn.multiclass.OneVsOneClassifier, + "ovr": sklearn.multiclass.OneVsRestClassifier, + } + if (cls := opts.get(self.strategy)) is None: + raise ValueError( + f"Expected `strategy` to be one of {list(opts)}, got {self.strategy}" + ) + X = input_to_host_array_with_sparse_support(X) y = input_to_host_array(y).array + with cuml.internals.exit_internal_api(): - self.multiclass_estimator.fit(X, y) - return self + wrapper = cls(self.estimator, n_jobs=None).fit(X, y) + + self.multiclass_estimator = wrapper + return self @generate_docstring( return_values={ @@ -149,8 +84,7 @@ def predict(self, X) -> CumlArray: return_values={ "name": "results", "type": "dense", - "description": "Decision function \ - values", + "description": "Decision function values", "shape": "(n_samples, 1)", } ) @@ -162,46 +96,26 @@ def decision_function(self, X) -> CumlArray: with cuml.internals.exit_internal_api(): return self.multiclass_estimator.decision_function(X) - @classmethod - def _get_param_names(cls): - return super()._get_param_names() + ["estimator", "strategy"] - -class OneVsRestClassifier(MulticlassClassifier): +class MulticlassClassifier(_BaseMulticlassClassifier): """ - Wrapper around Sckit-learn's class with the same name. The input can be - any kind of cuML compatible array, and the output type follows cuML's - output type configuration rules. + Wrapper around scikit-learn multiclass classifiers that allows to + choose different multiclass strategies. + + .. deprecated:: 25.12 + + This estimator was deprecated in 25.12 and will be removed in 26.02. + Please use OneVsOneClassifier or OneVsRestClassifier directly instead. + + The input can be any kind of cuML compatible array, and the output type + follows cuML's output type configuration rules. - Berofe passing the data to scikit-learn, it is converted to host (numpy) + Before passing the data to scikit-learn, it is converted to host (numpy) array. Under the hood the data is partitioned for binary classification, and it is transformed back to the device by the cuML estimator. These copies back and forth the device and the host have some overhead. For more details see issue https://github.com/rapidsai/cuml/issues/2876. - For documentation see `scikit-learn's OneVsRestClassifier - `_. - - Examples - -------- - - .. code-block:: python - - >>> from cuml.linear_model import LogisticRegression - >>> from cuml.multiclass import OneVsRestClassifier - >>> from cuml.datasets.classification import make_classification - - >>> X, y = make_classification(n_samples=10, n_features=6, - ... n_informative=4, n_classes=3, - ... random_state=137) - - >>> cls = OneVsRestClassifier(LogisticRegression()) - >>> cls.fit(X, y) - OneVsRestClassifier(estimator=LogisticRegression()) - >>> cls.predict(X) - array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) - - Parameters ---------- estimator : cuML estimator @@ -221,60 +135,129 @@ class OneVsRestClassifier(MulticlassClassifier): type. If None, the output type set at the module level (`cuml.global_settings.output_type`) will be used. See :ref:`output-data-type-configuration` for more info. + strategy: string {'ovr', 'ovo'}, default='ovr' + Multiclass classification strategy: 'ovr': one vs. rest or 'ovo': one + vs. one + + Attributes + ---------- + classes_ : float, shape (`n_classes_`) + Array of class labels. + n_classes_ : int + Number of classes. + + Examples + -------- + >>> from cuml.linear_model import LogisticRegression + >>> from cuml.multiclass import MulticlassClassifier + >>> from cuml.datasets.classification import make_classification + + >>> X, y = make_classification(n_samples=10, n_features=6, + ... n_informative=4, n_classes=3, + ... random_state=137) + + >>> cls = MulticlassClassifier(LogisticRegression(), strategy='ovo') + >>> cls.fit(X, y) + MulticlassClassifier(estimator=LogisticRegression()) + >>> cls.predict(X) + array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) """ def __init__( - self, estimator, *args, handle=None, verbose=False, output_type=None + self, + estimator, + *, + handle=None, + verbose=False, + output_type=None, + strategy="ovr", ): + warnings.warn( + "MulticlassClassifier was deprecated in version 25.12 and will be " + "removed in version 26.02. Please use OneVsOneClassifier or " + "OneVsRestClassifier directly instead.", + FutureWarning, + ) + super().__init__( - estimator, - *args, - handle=handle, - verbose=verbose, - output_type=output_type, - strategy="ovr", + estimator, handle=handle, verbose=verbose, output_type=output_type ) + self.strategy = strategy @classmethod def _get_param_names(cls): - param_names = super()._get_param_names() - param_names.remove("strategy") - return param_names + return [*super()._get_param_names(), "strategy"] -class OneVsOneClassifier(MulticlassClassifier): +class OneVsRestClassifier(_BaseMulticlassClassifier): """ Wrapper around Sckit-learn's class with the same name. The input can be any kind of cuML compatible array, and the output type follows cuML's output type configuration rules. - Berofe passing the data to scikit-learn, it is converted to host (numpy) + Before passing the data to scikit-learn, it is converted to host (numpy) array. Under the hood the data is partitioned for binary classification, and it is transformed back to the device by the cuML estimator. These copies back and forth the device and the host have some overhead. For more details see issue https://github.com/rapidsai/cuml/issues/2876. - For documentation see `scikit-learn's OneVsOneClassifier - `_. + For documentation see `scikit-learn's OneVsRestClassifier + `_. + + Parameters + ---------- + estimator : cuML estimator + handle : cuml.Handle + Specifies the cuml.handle that holds internal CUDA state for + computations in this model. Most importantly, this specifies the CUDA + stream that will be used for the model's computations, so users can + run different models concurrently in different streams by creating + handles in several streams. + If it is None, a new one is created. + verbose : int or boolean, default=False + Sets logging level. It must be one of `cuml.common.logger.level_*`. + See :ref:`verbosity-levels` for more info. + output_type : {'input', 'array', 'dataframe', 'series', 'df_obj', \ + 'numba', 'cupy', 'numpy', 'cudf', 'pandas'}, default=None + Return results and set estimator attributes to the indicated output + type. If None, the output type set at the module level + (`cuml.global_settings.output_type`) will be used. See + :ref:`output-data-type-configuration` for more info. Examples -------- + >>> from cuml.linear_model import LogisticRegression + >>> from cuml.multiclass import OneVsRestClassifier + >>> from cuml.datasets.classification import make_classification + + >>> X, y = make_classification(n_samples=10, n_features=6, + ... n_informative=4, n_classes=3, + ... random_state=137) + + >>> cls = OneVsRestClassifier(LogisticRegression()) + >>> cls.fit(X, y) + OneVsRestClassifier(estimator=LogisticRegression()) + >>> cls.predict(X) + array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) + """ + + strategy = "ovr" - .. code-block:: python - >>> from cuml.linear_model import LogisticRegression - >>> from cuml.multiclass import OneVsOneClassifier - >>> from cuml.datasets.classification import make_classification +class OneVsOneClassifier(_BaseMulticlassClassifier): + """ + Wrapper around Sckit-learn's class with the same name. The input can be + any kind of cuML compatible array, and the output type follows cuML's + output type configuration rules. - >>> X, y = make_classification(n_samples=10, n_features=6, - ... n_informative=4, n_classes=3, - ... random_state=137) + Before passing the data to scikit-learn, it is converted to host (numpy) + array. Under the hood the data is partitioned for binary classification, + and it is transformed back to the device by the cuML estimator. These + copies back and forth the device and the host have some overhead. For more + details see issue https://github.com/rapidsai/cuml/issues/2876. - >>> cls = OneVsOneClassifier(LogisticRegression()) - >>> cls.fit(X, y) - OneVsOneClassifier(estimator=LogisticRegression()) - >>> cls.predict(X) - array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) + For documentation see `scikit-learn's OneVsOneClassifier + `_. Parameters ---------- @@ -295,22 +278,22 @@ class OneVsOneClassifier(MulticlassClassifier): type. If None, the output type set at the module level (`cuml.global_settings.output_type`) will be used. See :ref:`output-data-type-configuration` for more info. - """ - def __init__( - self, estimator, *args, handle=None, verbose=False, output_type=None - ): - super().__init__( - estimator, - *args, - handle=handle, - verbose=verbose, - output_type=output_type, - strategy="ovo", - ) + Examples + -------- + >>> from cuml.linear_model import LogisticRegression + >>> from cuml.multiclass import OneVsOneClassifier + >>> from cuml.datasets.classification import make_classification + + >>> X, y = make_classification(n_samples=10, n_features=6, + ... n_informative=4, n_classes=3, + ... random_state=137) + + >>> cls = OneVsOneClassifier(LogisticRegression()) + >>> cls.fit(X, y) + OneVsOneClassifier(estimator=LogisticRegression()) + >>> cls.predict(X) + array([1, 1, 0, 1, 1, 1, 2, 2, 1, 2]) + """ - @classmethod - def _get_param_names(cls): - param_names = super()._get_param_names() - param_names.remove("strategy") - return param_names + strategy = "ovo" diff --git a/python/cuml/cuml/svm/svc.py b/python/cuml/cuml/svm/svc.py index d600742744..74c2fb859d 100644 --- a/python/cuml/cuml/svm/svc.py +++ b/python/cuml/cuml/svm/svc.py @@ -24,7 +24,7 @@ from cuml.internals.logger import warn from cuml.internals.mixins import ClassifierMixin from cuml.internals.utils import check_random_seed -from cuml.multiclass import MulticlassClassifier +from cuml.multiclass import OneVsOneClassifier, OneVsRestClassifier from cuml.svm.svm_base import SVMBase @@ -334,19 +334,24 @@ def _fit_multiclass(self, X, y, sample_weight) -> "SVC": ) params = self.get_params() - strategy = params.pop("decision_function_shape", "ovo") - self._multiclass = MulticlassClassifier( + decision_function_shape = params.pop("decision_function_shape") + wrappers = {"ovo": OneVsOneClassifier, "ovr": OneVsRestClassifier} + if (multiclass_cls := wrappers.get(decision_function_shape)) is None: + raise ValueError( + f"Expected `decision_function_shape` to be one of " + f"{list(wrappers)}, got {decision_function_shape}" + ) + self._multiclass = multiclass_cls( estimator=SVC(**params), handle=self.handle, verbose=self.verbose, output_type=self.output_type, - strategy=strategy, ) self._multiclass.fit(X, y) # if using one-vs-one we align support_ indices to those of # full dataset - if strategy == "ovo": + if decision_function_shape == "ovo": y = cp.array(y) classes = cp.unique(y) n_classes = len(classes) diff --git a/python/cuml/tests/test_multiclass.py b/python/cuml/tests/test_multiclass.py index 512ac3d60f..5160ac99b5 100644 --- a/python/cuml/tests/test_multiclass.py +++ b/python/cuml/tests/test_multiclass.py @@ -1,8 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2020-2025, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # -import sys - import numpy as np import pytest @@ -10,9 +8,6 @@ from cuml import multiclass as cu_multiclass from cuml.testing.datasets import make_classification_dataset -# As tests directory is not a module, we need to add it to the path -sys.path.insert(0, ".") - @pytest.mark.parametrize("strategy", ["ovr", "ovo"]) @pytest.mark.parametrize("use_wrapper", [True, False]) @@ -36,7 +31,11 @@ def test_logistic_regression( culog = cuLog() if use_wrapper: - cls = cu_multiclass.MulticlassClassifier(culog, strategy=strategy) + with pytest.warns( + FutureWarning, + match="MulticlassClassifier was deprecated", + ): + cls = cu_multiclass.MulticlassClassifier(culog, strategy=strategy) else: if strategy == "ovo": cls = cu_multiclass.OneVsOneClassifier(culog)