CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
multiclass.py585 linesDownload Raw Back to utils
1"""Utilities to handle multiclass/multioutput target in classifiers."""
2
3# Authors: The scikit-learn developers
4# SPDX-License-Identifier: BSD-3-Clause
5
6import warnings
7from collections.abc import Sequence
8from itertools import chain
9
10import numpy as np
11from scipy.sparse import issparse
12
13from ..utils._array_api import get_namespace
14from ..utils.fixes import VisibleDeprecationWarning
15from ._unique import attach_unique, cached_unique
16from .validation import _assert_all_finite, check_array
17
18
19def _unique_multiclass(y, xp=None):
20    xp, is_array_api_compliant = get_namespace(y, xp=xp)
21    if hasattr(y, "__array__") or is_array_api_compliant:
22        return cached_unique(xp.asarray(y), xp=xp)
23    else:
24        return set(y)
25
26
27def _unique_indicator(y, xp=None):
28    xp, _ = get_namespace(y, xp=xp)
29    return xp.arange(
30        check_array(y, input_name="y", accept_sparse=["csr", "csc", "coo"]).shape[1]
31    )
32
33
34_FN_UNIQUE_LABELS = {
35    "binary": _unique_multiclass,
36    "multiclass": _unique_multiclass,
37    "multilabel-indicator": _unique_indicator,
38}
39
40
41def unique_labels(*ys):
42    """Extract an ordered array of unique labels.
43
44    We don't allow:
45        - mix of multilabel and multiclass (single label) targets
46        - mix of label indicator matrix and anything else,
47          because there are no explicit labels)
48        - mix of label indicator matrices of different sizes
49        - mix of string and integer labels
50
51    At the moment, we also don't allow "multiclass-multioutput" input type.
52
53    Parameters
54    ----------
55    *ys : array-likes
56        Label values.
57
58    Returns
59    -------
60    out : ndarray of shape (n_unique_labels,)
61        An ordered array of unique labels.
62
63    Examples
64    --------
65    >>> from sklearn.utils.multiclass import unique_labels
66    >>> unique_labels([3, 5, 5, 5, 7, 7])
67    array([3, 5, 7])
68    >>> unique_labels([1, 2, 3, 4], [2, 2, 3, 4])
69    array([1, 2, 3, 4])
70    >>> unique_labels([1, 2, 10], [5, 11])
71    array([ 1,  2,  5, 10, 11])
72    """
73    ys = attach_unique(*ys, return_tuple=True)
74    xp, is_array_api_compliant = get_namespace(*ys)
75    if len(ys) == 0:
76        raise ValueError("No argument has been passed.")
77    # Check that we don't mix label format
78
79    ys_types = set(type_of_target(x) for x in ys)
80    if ys_types == {"binary", "multiclass"}:
81        ys_types = {"multiclass"}
82
83    if len(ys_types) > 1:
84        raise ValueError("Mix type of y not allowed, got types %s" % ys_types)
85
86    label_type = ys_types.pop()
87
88    # Check consistency for the indicator format
89    if (
90        label_type == "multilabel-indicator"
91        and len(
92            set(
93                check_array(y, accept_sparse=["csr", "csc", "coo"]).shape[1] for y in ys
94            )
95        )
96        > 1
97    ):
98        raise ValueError(
99            "Multi-label binary indicator input with different numbers of labels"
100        )
101
102    # Get the unique set of labels
103    _unique_labels = _FN_UNIQUE_LABELS.get(label_type, None)
104    if not _unique_labels:
105        raise ValueError("Unknown label type: %s" % repr(ys))
106
107    if is_array_api_compliant:
108        # array_api does not allow for mixed dtypes
109        unique_ys = xp.concat([_unique_labels(y, xp=xp) for y in ys])
110        return xp.unique_values(unique_ys)
111
112    ys_labels = set(
113        chain.from_iterable((i for i in _unique_labels(y, xp=xp)) for y in ys)
114    )
115    # Check that we don't mix string type with number type
116    if len(set(isinstance(label, str) for label in ys_labels)) > 1:
117        raise ValueError("Mix of label input types (string and number)")
118
119    return xp.asarray(sorted(ys_labels))
120
121
122def _is_integral_float(y):
123    xp, is_array_api_compliant = get_namespace(y)
124    return xp.isdtype(y.dtype, "real floating") and bool(
125        xp.all(xp.astype((xp.astype(y, xp.int64)), y.dtype) == y)
126    )
127
128
129def is_multilabel(y):
130    """Check if ``y`` is in a multilabel format.
131
132    Parameters
133    ----------
134    y : ndarray of shape (n_samples,)
135        Target values.
136
137    Returns
138    -------
139    out : bool
140        Return ``True``, if ``y`` is in a multilabel format, else ``False``.
141
142    Examples
143    --------
144    >>> import numpy as np
145    >>> from sklearn.utils.multiclass import is_multilabel
146    >>> is_multilabel([0, 1, 0, 1])
147    False
148    >>> is_multilabel([[1], [0, 2], []])
149    False
150    >>> is_multilabel(np.array([[1, 0], [0, 0]]))
151    True
152    >>> is_multilabel(np.array([[1], [0], [0]]))
153    False
154    >>> is_multilabel(np.array([[1, 0, 0]]))
155    True
156    """
157    xp, is_array_api_compliant = get_namespace(y)
158    if hasattr(y, "__array__") or isinstance(y, Sequence) or is_array_api_compliant:
159        # DeprecationWarning will be replaced by ValueError, see NEP 34
160        # https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
161        check_y_kwargs = dict(
162            accept_sparse=True,
163            allow_nd=True,
164            ensure_all_finite=False,
165            ensure_2d=False,
166            ensure_min_samples=0,
167            ensure_min_features=0,
168        )
169        with warnings.catch_warnings():
170            warnings.simplefilter("error", VisibleDeprecationWarning)
171            try:
172                y = check_array(y, dtype=None, **check_y_kwargs)
173            except (VisibleDeprecationWarning, ValueError) as e:
174                if str(e).startswith("Complex data not supported"):
175                    raise
176
177                # dtype=object should be provided explicitly for ragged arrays,
178                # see NEP 34
179                y = check_array(y, dtype=object, **check_y_kwargs)
180
181    if not (hasattr(y, "shape") and y.ndim == 2 and y.shape[1] > 1):
182        return False
183
184    if issparse(y):
185        if y.format in ("dok", "lil"):
186            y = y.tocsr()
187        labels = xp.unique_values(y.data)
188        return len(y.data) == 0 or (
189            (labels.size == 1 or ((labels.size == 2) and (0 in labels)))
190            and (y.dtype.kind in "biu" or _is_integral_float(labels))  # bool, int, uint
191        )
192    else:
193        labels = cached_unique(y, xp=xp)
194
195        return labels.shape[0] < 3 and (
196            xp.isdtype(y.dtype, ("bool", "signed integer", "unsigned integer"))
197            or _is_integral_float(labels)
198        )
199
200
201def check_classification_targets(y):
202    """Ensure that target y is of a non-regression type.
203
204    Only the following target types (as defined in type_of_target) are allowed:
205        'binary', 'multiclass', 'multiclass-multioutput',
206        'multilabel-indicator', 'multilabel-sequences'
207
208    Parameters
209    ----------
210    y : array-like
211        Target values.
212    """
213    y_type = type_of_target(y, input_name="y")
214    if y_type not in [
215        "binary",
216        "multiclass",
217        "multiclass-multioutput",
218        "multilabel-indicator",
219        "multilabel-sequences",
220    ]:
221        raise ValueError(
222            f"Unknown label type: {y_type}. Maybe you are trying to fit a "
223            "classifier, which expects discrete classes on a "
224            "regression target with continuous values."
225        )
226
227
228def type_of_target(y, input_name="", raise_unknown=False):
229    """Determine the type of data indicated by the target.
230
231    Note that this type is the most specific type that can be inferred.
232    For example:
233
234    * ``binary`` is more specific but compatible with ``multiclass``.
235    * ``multiclass`` of integers is more specific but compatible with ``continuous``.
236    * ``multilabel-indicator`` is more specific but compatible with
237      ``multiclass-multioutput``.
238
239    Parameters
240    ----------
241    y : {array-like, sparse matrix}
242        Target values. If a sparse matrix, `y` is expected to be a
243        CSR/CSC matrix.
244
245    input_name : str, default=""
246        The data name used to construct the error message.
247
248        .. versionadded:: 1.1.0
249
250    raise_unknown : bool, default=False
251        If `True`, raise an error when the type of target returned by
252        :func:`~sklearn.utils.multiclass.type_of_target` is `"unknown"`.
253
254        .. versionadded:: 1.6
255
256    Returns
257    -------
258    target_type : str
259        One of:
260
261        * 'continuous': `y` is an array-like of floats that are not all
262          integers, and is 1d or a column vector.
263        * 'continuous-multioutput': `y` is a 2d array of floats that are
264          not all integers, and both dimensions are of size > 1.
265        * 'binary': `y` contains <= 2 discrete values and is 1d or a column
266          vector.
267        * 'multiclass': `y` contains more than two discrete values, is not a
268          sequence of sequences, and is 1d or a column vector.
269        * 'multiclass-multioutput': `y` is a 2d array that contains more
270          than two discrete values, is not a sequence of sequences, and both
271          dimensions are of size > 1.
272        * 'multilabel-indicator': `y` is a label indicator matrix, an array
273          of two dimensions with at least two columns, and at most 2 unique
274          values.
275        * 'unknown': `y` is array-like but none of the above, such as a 3d
276          array, sequence of sequences, or an array of non-sequence objects.
277
278    Examples
279    --------
280    >>> from sklearn.utils.multiclass import type_of_target
281    >>> import numpy as np
282    >>> type_of_target([0.1, 0.6])
283    'continuous'
284    >>> type_of_target([1, -1, -1, 1])
285    'binary'
286    >>> type_of_target(['a', 'b', 'a'])
287    'binary'
288    >>> type_of_target([1.0, 2.0])
289    'binary'
290    >>> type_of_target([1, 0, 2])
291    'multiclass'
292    >>> type_of_target([1.0, 0.0, 3.0])
293    'multiclass'
294    >>> type_of_target(['a', 'b', 'c'])
295    'multiclass'
296    >>> type_of_target(np.array([[1, 2], [3, 1]]))
297    'multiclass-multioutput'
298    >>> type_of_target([[1, 2]])
299    'multilabel-indicator'
300    >>> type_of_target(np.array([[1.5, 2.0], [3.0, 1.6]]))
301    'continuous-multioutput'
302    >>> type_of_target(np.array([[0, 1], [1, 1]]))
303    'multilabel-indicator'
304    """
305    xp, is_array_api_compliant = get_namespace(y)
306
307    def _raise_or_return():
308        """Depending on the value of raise_unknown, either raise an error or return
309        'unknown'.
310        """
311        if raise_unknown:
312            input = input_name if input_name else "data"
313            raise ValueError(f"Unknown label type for {input}: {y!r}")
314        else:
315            return "unknown"
316
317    valid = (
318        (isinstance(y, Sequence) or issparse(y) or hasattr(y, "__array__"))
319        and not isinstance(y, str)
320    ) or is_array_api_compliant
321
322    if not valid:
323        raise ValueError(
324            "Expected array-like (array or non-string sequence), got %r" % y
325        )
326
327    sparse_pandas = y.__class__.__name__ in ["SparseSeries", "SparseArray"]
328    if sparse_pandas:
329        raise ValueError("y cannot be class 'SparseSeries' or 'SparseArray'")
330
331    if is_multilabel(y):
332        return "multilabel-indicator"
333
334    # DeprecationWarning will be replaced by ValueError, see NEP 34
335    # https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
336    # We therefore catch both deprecation (NumPy < 1.24) warning and
337    # value error (NumPy >= 1.24).
338    check_y_kwargs = dict(
339        accept_sparse=True,
340        allow_nd=True,
341        ensure_all_finite=False,
342        ensure_2d=False,
343        ensure_min_samples=0,
344        ensure_min_features=0,
345    )
346
347    with warnings.catch_warnings():
348        warnings.simplefilter("error", VisibleDeprecationWarning)
349        if not issparse(y):
350            try:
351                y = check_array(y, dtype=None, **check_y_kwargs)
352            except (VisibleDeprecationWarning, ValueError) as e:
353                if str(e).startswith("Complex data not supported"):
354                    raise
355
356                # dtype=object should be provided explicitly for ragged arrays,
357                # see NEP 34
358                y = check_array(y, dtype=object, **check_y_kwargs)
359
360    try:
361        first_row_or_val = y[[0], :] if issparse(y) else y[0]
362        # labels in bytes format
363        if isinstance(first_row_or_val, bytes):
364            raise TypeError(
365                "Support for labels represented as bytes is not supported. Convert "
366                "the labels to a string or integer format."
367            )
368        # The old sequence of sequences format
369        if (
370            not hasattr(first_row_or_val, "__array__")
371            and isinstance(first_row_or_val, Sequence)
372            and not isinstance(first_row_or_val, str)
373        ):
374            raise ValueError(
375                "You appear to be using a legacy multi-label data"
376                " representation. Sequence of sequences are no"
377                " longer supported; use a binary array or sparse"
378                " matrix instead - the MultiLabelBinarizer"
379                " transformer can convert to this format."
380            )
381    except IndexError:
382        pass
383
384    # Invalid inputs
385    if y.ndim not in (1, 2):
386        # Number of dimension greater than 2: [[[1, 2]]]
387        return _raise_or_return()
388    if not min(y.shape):
389        # Empty ndarray: []/[[]]
390        if y.ndim == 1:
391            # 1-D empty array: []
392            return "binary"  # []
393        # 2-D empty array: [[]]
394        return _raise_or_return()
395    if not issparse(y) and y.dtype == object and not isinstance(y.flat[0], str):
396        # [obj_1] and not ["label_1"]
397        return _raise_or_return()
398
399    # Check if multioutput
400    if y.ndim == 2 and y.shape[1] > 1:
401        suffix = "-multioutput"  # [[1, 2], [1, 2]]
402    else:
403        suffix = ""  # [1, 2, 3] or [[1], [2], [3]]
404
405    # Check float and contains non-integer float values
406    if xp.isdtype(y.dtype, "real floating"):
407        # [.1, .2, 3] or [[.1, .2, 3]] or [[1., .2]] and not [1., 2., 3.]
408        data = y.data if issparse(y) else y
409        if xp.any(data != xp.astype(data, int)):
410            _assert_all_finite(data, input_name=input_name)
411            return "continuous" + suffix
412
413    # Check multiclass
414    if issparse(first_row_or_val):
415        first_row_or_val = first_row_or_val.data
416    classes = cached_unique(y)
417    if y.shape[0] > 20 and y.shape[0] > classes.shape[0] > round(0.5 * y.shape[0]):
418        # Only raise the warning when we have at least 20 samples.
419        warnings.warn(
420            "The number of unique classes is greater than 50% of the number "
421            "of samples. `y` could represent a regression problem, not a "
422            "classification problem.",
423            UserWarning,
424            stacklevel=2,
425        )
426    if classes.shape[0] > 2 or (y.ndim == 2 and len(first_row_or_val) > 1):
427        # [1, 2, 3] or [[1., 2., 3]] or [[1, 2]]
428        return "multiclass" + suffix
429    else:
430        return "binary"  # [1, 2] or [["a"], ["b"]]
431
432
433def _check_partial_fit_first_call(clf, classes=None):
434    """Private helper function for factorizing common classes param logic.
435
436    Estimators that implement the ``partial_fit`` API need to be provided with
437    the list of possible classes at the first call to partial_fit.
438
439    Subsequent calls to partial_fit should check that ``classes`` is still
440    consistent with a previous value of ``clf.classes_`` when provided.
441
442    This function returns True if it detects that this was the first call to
443    ``partial_fit`` on ``clf``. In that case the ``classes_`` attribute is also
444    set on ``clf``.
445
446    """
447    if getattr(clf, "classes_", None) is None and classes is None:
448        raise ValueError("classes must be passed on the first call to partial_fit.")
449
450    elif classes is not None:
451        if getattr(clf, "classes_", None) is not None:
452            if not np.array_equal(clf.classes_, unique_labels(classes)):
453                raise ValueError(
454                    "`classes=%r` is not the same as on last call "
455                    "to partial_fit, was: %r" % (classes, clf.classes_)
456                )
457
458        else:
459            # This is the first call to partial_fit
460            clf.classes_ = unique_labels(classes)
461            return True
462
463    # classes is None and clf.classes_ has already previously been set:
464    # nothing to do
465    return False
466
467
468def class_distribution(y, sample_weight=None):
469    """Compute class priors from multioutput-multiclass target data.
470
471    Parameters
472    ----------
473    y : {array-like, sparse matrix} of size (n_samples, n_outputs)
474        The labels for each example.
475
476    sample_weight : array-like of shape (n_samples,), default=None
477        Sample weights.
478
479    Returns
480    -------
481    classes : list of size n_outputs of ndarray of size (n_classes,)
482        List of classes for each column.
483
484    n_classes : list of int of size n_outputs
485        Number of classes in each column.
486
487    class_prior : list of size n_outputs of ndarray of size (n_classes,)
488        Class distribution of each column.
489    """
490    classes = []
491    n_classes = []
492    class_prior = []
493
494    n_samples, n_outputs = y.shape
495    if sample_weight is not None:
496        sample_weight = np.asarray(sample_weight)
497
498    if issparse(y):
499        y = y.tocsc()
500        y_nnz = np.diff(y.indptr)
501
502        for k in range(n_outputs):
503            col_nonzero = y.indices[y.indptr[k] : y.indptr[k + 1]]
504            # separate sample weights for zero and non-zero elements
505            if sample_weight is not None:
506                nz_samp_weight = sample_weight[col_nonzero]
507                zeros_samp_weight_sum = np.sum(sample_weight) - np.sum(nz_samp_weight)
508            else:
509                nz_samp_weight = None
510                zeros_samp_weight_sum = y.shape[0] - y_nnz[k]
511
512            classes_k, y_k = np.unique(
513                y.data[y.indptr[k] : y.indptr[k + 1]], return_inverse=True
514            )
515            class_prior_k = np.bincount(y_k, weights=nz_samp_weight)
516
517            # An explicit zero was found, combine its weight with the weight
518            # of the implicit zeros
519            if 0 in classes_k:
520                class_prior_k[classes_k == 0] += zeros_samp_weight_sum
521
522            # If an there is an implicit zero and it is not in classes and
523            # class_prior, make an entry for it
524            if 0 not in classes_k and y_nnz[k] < y.shape[0]:
525                classes_k = np.insert(classes_k, 0, 0)
526                class_prior_k = np.insert(class_prior_k, 0, zeros_samp_weight_sum)
527
528            classes.append(classes_k)
529            n_classes.append(classes_k.shape[0])
530            class_prior.append(class_prior_k / class_prior_k.sum())
531    else:
532        for k in range(n_outputs):
533            classes_k, y_k = np.unique(y[:, k], return_inverse=True)
534            classes.append(classes_k)
535            n_classes.append(classes_k.shape[0])
536            class_prior_k = np.bincount(y_k, weights=sample_weight)
537            class_prior.append(class_prior_k / class_prior_k.sum())
538
539    return (classes, n_classes, class_prior)
540
541
542def _ovr_decision_function(predictions, confidences, n_classes):
543    """Compute a continuous, tie-breaking OvR decision function from OvO.
544
545    It is important to include a continuous value, not only votes,
546    to make computing AUC or calibration meaningful.
547
548    Parameters
549    ----------
550    predictions : array-like of shape (n_samples, n_classifiers)
551        Predicted classes for each binary classifier.
552
553    confidences : array-like of shape (n_samples, n_classifiers)
554        Decision functions or predicted probabilities for positive class
555        for each binary classifier.
556
557    n_classes : int
558        Number of classes. n_classifiers must be
559        ``n_classes * (n_classes - 1 ) / 2``.
560    """
561    n_samples = predictions.shape[0]
562    votes = np.zeros((n_samples, n_classes))
563    sum_of_confidences = np.zeros((n_samples, n_classes))
564
565    k = 0
566    for i in range(n_classes):
567        for j in range(i + 1, n_classes):
568            sum_of_confidences[:, i] -= confidences[:, k]
569            sum_of_confidences[:, j] += confidences[:, k]
570            votes[predictions[:, k] == 0, i] += 1
571            votes[predictions[:, k] == 1, j] += 1
572            k += 1
573
574    # Monotonically transform the sum_of_confidences to (-1/3, 1/3)
575    # and add it with votes. The monotonic transformation  is
576    # f: x -> x / (3 * (|x| + 1)), it uses 1/3 instead of 1/2
577    # to ensure that we won't reach the limits and change vote order.
578    # The motivation is to use confidence levels as a way to break ties in
579    # the votes without switching any decision made based on a difference
580    # of 1 vote.
581    transformed_confidences = sum_of_confidences / (
582        3 * (np.abs(sum_of_confidences) + 1)
583    )
584    return votes + transformed_confidences
585 
Aluode/PerceptionLabPortable · CoolFace