CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_sequential.py364 linesDownload Raw Back to feature_selection
1"""
2Sequential feature selection
3"""
4
5# Authors: The scikit-learn developers
6# SPDX-License-Identifier: BSD-3-Clause
7
8from numbers import Integral, Real
9
10import numpy as np
11
12from ..base import BaseEstimator, MetaEstimatorMixin, _fit_context, clone, is_classifier
13from ..metrics import check_scoring, get_scorer_names
14from ..model_selection import check_cv, cross_val_score
15from ..utils._metadata_requests import (
16    MetadataRouter,
17    MethodMapping,
18    _raise_for_params,
19    _routing_enabled,
20    process_routing,
21)
22from ..utils._param_validation import HasMethods, Interval, RealNotInt, StrOptions
23from ..utils._tags import get_tags
24from ..utils.validation import check_is_fitted, validate_data
25from ._base import SelectorMixin
26
27
28class SequentialFeatureSelector(SelectorMixin, MetaEstimatorMixin, BaseEstimator):
29    """Transformer that performs Sequential Feature Selection.
30
31    This Sequential Feature Selector adds (forward selection) or
32    removes (backward selection) features to form a feature subset in a
33    greedy fashion. At each stage, this estimator chooses the best feature to
34    add or remove based on the cross-validation score of an estimator. In
35    the case of unsupervised learning, this Sequential Feature Selector
36    looks only at the features (X), not the desired outputs (y).
37
38    Read more in the :ref:`User Guide <sequential_feature_selection>`.
39
40    .. versionadded:: 0.24
41
42    Parameters
43    ----------
44    estimator : estimator instance
45        An unfitted estimator.
46
47    n_features_to_select : "auto", int or float, default="auto"
48        If `"auto"`, the behaviour depends on the `tol` parameter:
49
50        - if `tol` is not `None`, then features are selected while the score
51          change does not exceed `tol`.
52        - otherwise, half of the features are selected.
53
54        If integer, the parameter is the absolute number of features to select.
55        If float between 0 and 1, it is the fraction of features to select.
56
57        .. versionadded:: 1.1
58           The option `"auto"` was added in version 1.1.
59
60        .. versionchanged:: 1.3
61           The default changed from `"warn"` to `"auto"` in 1.3.
62
63    tol : float, default=None
64        If the score is not incremented by at least `tol` between two
65        consecutive feature additions or removals, stop adding or removing.
66
67        `tol` can be negative when removing features using `direction="backward"`.
68        `tol` is required to be strictly positive when doing forward selection.
69        It can be useful to reduce the number of features at the cost of a small
70        decrease in the score.
71
72        `tol` is enabled only when `n_features_to_select` is `"auto"`.
73
74        .. versionadded:: 1.1
75
76    direction : {'forward', 'backward'}, default='forward'
77        Whether to perform forward selection or backward selection.
78
79    scoring : str or callable, default=None
80        Scoring method to use for cross-validation. Options:
81
82        - str: see :ref:`scoring_string_names` for options.
83        - callable: a scorer callable object (e.g., function) with signature
84          ``scorer(estimator, X, y)`` that returns a single value.
85          See :ref:`scoring_callable` for details.
86        - `None`: the `estimator`'s
87          :ref:`default evaluation criterion <scoring_api_overview>` is used.
88
89    cv : int, cross-validation generator or an iterable, default=None
90        Determines the cross-validation splitting strategy.
91        Possible inputs for cv are:
92
93        - None, to use the default 5-fold cross validation,
94        - integer, to specify the number of folds in a `(Stratified)KFold`,
95        - :term:`CV splitter`,
96        - An iterable yielding (train, test) splits as arrays of indices.
97
98        For integer/None inputs, if the estimator is a classifier and ``y`` is
99        either binary or multiclass,
100        :class:`~sklearn.model_selection.StratifiedKFold` is used. In all other
101        cases, :class:`~sklearn.model_selection.KFold` is used. These splitters
102        are instantiated with `shuffle=False` so the splits will be the same
103        across calls.
104
105        Refer :ref:`User Guide <cross_validation>` for the various
106        cross-validation strategies that can be used here.
107
108    n_jobs : int, default=None
109        Number of jobs to run in parallel. When evaluating a new feature to
110        add or remove, the cross-validation procedure is parallel over the
111        folds.
112        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
113        ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
114        for more details.
115
116    Attributes
117    ----------
118    n_features_in_ : int
119        Number of features seen during :term:`fit`. Only defined if the
120        underlying estimator exposes such an attribute when fit.
121
122        .. versionadded:: 0.24
123
124    feature_names_in_ : ndarray of shape (`n_features_in_`,)
125        Names of features seen during :term:`fit`. Defined only when `X`
126        has feature names that are all strings.
127
128        .. versionadded:: 1.0
129
130    n_features_to_select_ : int
131        The number of features that were selected.
132
133    support_ : ndarray of shape (n_features,), dtype=bool
134        The mask of selected features.
135
136    See Also
137    --------
138    GenericUnivariateSelect : Univariate feature selector with configurable
139        strategy.
140    RFE : Recursive feature elimination based on importance weights.
141    RFECV : Recursive feature elimination based on importance weights, with
142        automatic selection of the number of features.
143    SelectFromModel : Feature selection based on thresholds of importance
144        weights.
145
146    Examples
147    --------
148    >>> from sklearn.feature_selection import SequentialFeatureSelector
149    >>> from sklearn.neighbors import KNeighborsClassifier
150    >>> from sklearn.datasets import load_iris
151    >>> X, y = load_iris(return_X_y=True)
152    >>> knn = KNeighborsClassifier(n_neighbors=3)
153    >>> sfs = SequentialFeatureSelector(knn, n_features_to_select=3)
154    >>> sfs.fit(X, y)
155    SequentialFeatureSelector(estimator=KNeighborsClassifier(n_neighbors=3),
156                              n_features_to_select=3)
157    >>> sfs.get_support()
158    array([ True, False,  True,  True])
159    >>> sfs.transform(X).shape
160    (150, 3)
161    """
162
163    _parameter_constraints: dict = {
164        "estimator": [HasMethods(["fit"])],
165        "n_features_to_select": [
166            StrOptions({"auto"}),
167            Interval(RealNotInt, 0, 1, closed="right"),
168            Interval(Integral, 0, None, closed="neither"),
169        ],
170        "tol": [None, Interval(Real, None, None, closed="neither")],
171        "direction": [StrOptions({"forward", "backward"})],
172        "scoring": [None, StrOptions(set(get_scorer_names())), callable],
173        "cv": ["cv_object"],
174        "n_jobs": [None, Integral],
175    }
176
177    def __init__(
178        self,
179        estimator,
180        *,
181        n_features_to_select="auto",
182        tol=None,
183        direction="forward",
184        scoring=None,
185        cv=5,
186        n_jobs=None,
187    ):
188        self.estimator = estimator
189        self.n_features_to_select = n_features_to_select
190        self.tol = tol
191        self.direction = direction
192        self.scoring = scoring
193        self.cv = cv
194        self.n_jobs = n_jobs
195
196    @_fit_context(
197        # SequentialFeatureSelector.estimator is not validated yet
198        prefer_skip_nested_validation=False
199    )
200    def fit(self, X, y=None, **params):
201        """Learn the features to select from X.
202
203        Parameters
204        ----------
205        X : array-like of shape (n_samples, n_features)
206            Training vectors, where `n_samples` is the number of samples and
207            `n_features` is the number of predictors.
208
209        y : array-like of shape (n_samples,), default=None
210            Target values. This parameter may be ignored for
211            unsupervised learning.
212
213        **params : dict, default=None
214            Parameters to be passed to the underlying `estimator`, `cv`
215            and `scorer` objects.
216
217            .. versionadded:: 1.6
218
219                Only available if `enable_metadata_routing=True`,
220                which can be set by using
221                ``sklearn.set_config(enable_metadata_routing=True)``.
222                See :ref:`Metadata Routing User Guide <metadata_routing>` for
223                more details.
224
225        Returns
226        -------
227        self : object
228            Returns the instance itself.
229        """
230        _raise_for_params(params, self, "fit")
231        tags = self.__sklearn_tags__()
232        X = validate_data(
233            self,
234            X,
235            accept_sparse="csc",
236            ensure_min_features=2,
237            ensure_all_finite=not tags.input_tags.allow_nan,
238        )
239        n_features = X.shape[1]
240
241        if self.n_features_to_select == "auto":
242            if self.tol is not None:
243                # With auto feature selection, `n_features_to_select_` will be updated
244                # to `support_.sum()` after features are selected.
245                self.n_features_to_select_ = n_features - 1
246            else:
247                self.n_features_to_select_ = n_features // 2
248        elif isinstance(self.n_features_to_select, Integral):
249            if self.n_features_to_select >= n_features:
250                raise ValueError("n_features_to_select must be < n_features.")
251            self.n_features_to_select_ = self.n_features_to_select
252        elif isinstance(self.n_features_to_select, Real):
253            self.n_features_to_select_ = int(n_features * self.n_features_to_select)
254
255        if self.tol is not None and self.tol < 0 and self.direction == "forward":
256            raise ValueError(
257                "tol must be strictly positive when doing forward selection"
258            )
259
260        cv = check_cv(self.cv, y, classifier=is_classifier(self.estimator))
261
262        cloned_estimator = clone(self.estimator)
263
264        # the current mask corresponds to the set of features:
265        # - that we have already *selected* if we do forward selection
266        # - that we have already *excluded* if we do backward selection
267        current_mask = np.zeros(shape=n_features, dtype=bool)
268        n_iterations = (
269            self.n_features_to_select_
270            if self.n_features_to_select == "auto" or self.direction == "forward"
271            else n_features - self.n_features_to_select_
272        )
273
274        old_score = -np.inf
275        is_auto_select = self.tol is not None and self.n_features_to_select == "auto"
276
277        # We only need to verify the routing here and not use the routed params
278        # because internally the actual routing will also take place inside the
279        # `cross_val_score` function.
280        if _routing_enabled():
281            process_routing(self, "fit", **params)
282        for _ in range(n_iterations):
283            new_feature_idx, new_score = self._get_best_new_feature_score(
284                cloned_estimator, X, y, cv, current_mask, **params
285            )
286            if is_auto_select and ((new_score - old_score) < self.tol):
287                break
288
289            old_score = new_score
290            current_mask[new_feature_idx] = True
291
292        if self.direction == "backward":
293            current_mask = ~current_mask
294
295        self.support_ = current_mask
296        self.n_features_to_select_ = self.support_.sum()
297
298        return self
299
300    def _get_best_new_feature_score(self, estimator, X, y, cv, current_mask, **params):
301        # Return the best new feature and its score to add to the current_mask,
302        # i.e. return the best new feature and its score to add (resp. remove)
303        # when doing forward selection (resp. backward selection).
304        # Feature will be added if the current score and past score are greater
305        # than tol when n_feature is auto,
306        candidate_feature_indices = np.flatnonzero(~current_mask)
307        scores = {}
308        for feature_idx in candidate_feature_indices:
309            candidate_mask = current_mask.copy()
310            candidate_mask[feature_idx] = True
311            if self.direction == "backward":
312                candidate_mask = ~candidate_mask
313            X_new = X[:, candidate_mask]
314            scores[feature_idx] = cross_val_score(
315                estimator,
316                X_new,
317                y,
318                cv=cv,
319                scoring=self.scoring,
320                n_jobs=self.n_jobs,
321                params=params,
322            ).mean()
323        new_feature_idx = max(scores, key=lambda feature_idx: scores[feature_idx])
324        return new_feature_idx, scores[new_feature_idx]
325
326    def _get_support_mask(self):
327        check_is_fitted(self)
328        return self.support_
329
330    def __sklearn_tags__(self):
331        tags = super().__sklearn_tags__()
332        tags.input_tags.allow_nan = get_tags(self.estimator).input_tags.allow_nan
333        tags.input_tags.sparse = get_tags(self.estimator).input_tags.sparse
334        return tags
335
336    def get_metadata_routing(self):
337        """Get metadata routing of this object.
338
339        Please check :ref:`User Guide <metadata_routing>` on how the routing
340        mechanism works.
341
342        .. versionadded:: 1.6
343
344        Returns
345        -------
346        routing : MetadataRouter
347            A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
348            routing information.
349        """
350        router = MetadataRouter(owner=self.__class__.__name__)
351        router.add(
352            estimator=self.estimator,
353            method_mapping=MethodMapping().add(caller="fit", callee="fit"),
354        )
355        router.add(
356            splitter=check_cv(self.cv, classifier=is_classifier(self.estimator)),
357            method_mapping=MethodMapping().add(caller="fit", callee="split"),
358        )
359        router.add(
360            scorer=check_scoring(self.estimator, scoring=self.scoring),
361            method_mapping=MethodMapping().add(caller="fit", callee="score"),
362        )
363        return router
364 
Aluode/PerceptionLabPortable · CoolFace