CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_pca.py858 linesDownload Raw Back to decomposition
1"""Principal Component Analysis."""
2
3# Authors: The scikit-learn developers
4# SPDX-License-Identifier: BSD-3-Clause
5
6from math import lgamma, log, sqrt
7from numbers import Integral, Real
8
9import numpy as np
10from scipy import linalg
11from scipy.sparse import issparse
12from scipy.sparse.linalg import svds
13
14from ..base import _fit_context
15from ..utils import check_random_state
16from ..utils._arpack import _init_arpack_v0
17from ..utils._array_api import _convert_to_numpy, get_namespace
18from ..utils._param_validation import Interval, RealNotInt, StrOptions
19from ..utils.extmath import _randomized_svd, fast_logdet, stable_cumsum, svd_flip
20from ..utils.sparsefuncs import _implicit_column_offset, mean_variance_axis
21from ..utils.validation import check_is_fitted, validate_data
22from ._base import _BasePCA
23
24
25def _assess_dimension(spectrum, rank, n_samples):
26    """Compute the log-likelihood of a rank ``rank`` dataset.
27
28    The dataset is assumed to be embedded in gaussian noise of shape(n,
29    dimf) having spectrum ``spectrum``. This implements the method of
30    T. P. Minka.
31
32    Parameters
33    ----------
34    spectrum : ndarray of shape (n_features,)
35        Data spectrum.
36    rank : int
37        Tested rank value. It should be strictly lower than n_features,
38        otherwise the method isn't specified (division by zero in equation
39        (31) from the paper).
40    n_samples : int
41        Number of samples.
42
43    Returns
44    -------
45    ll : float
46        The log-likelihood.
47
48    References
49    ----------
50    This implements the method of `Thomas P. Minka:
51    Automatic Choice of Dimensionality for PCA. NIPS 2000: 598-604
52    <https://proceedings.neurips.cc/paper/2000/file/7503cfacd12053d309b6bed5c89de212-Paper.pdf>`_
53    """
54    xp, _ = get_namespace(spectrum)
55
56    n_features = spectrum.shape[0]
57    if not 1 <= rank < n_features:
58        raise ValueError("the tested rank should be in [1, n_features - 1]")
59
60    eps = 1e-15
61
62    if spectrum[rank - 1] < eps:
63        # When the tested rank is associated with a small eigenvalue, there's
64        # no point in computing the log-likelihood: it's going to be very
65        # small and won't be the max anyway. Also, it can lead to numerical
66        # issues below when computing pa, in particular in log((spectrum[i] -
67        # spectrum[j]) because this will take the log of something very small.
68        return -xp.inf
69
70    pu = -rank * log(2.0)
71    for i in range(1, rank + 1):
72        pu += (
73            lgamma((n_features - i + 1) / 2.0) - log(xp.pi) * (n_features - i + 1) / 2.0
74        )
75
76    pl = xp.sum(xp.log(spectrum[:rank]))
77    pl = -pl * n_samples / 2.0
78
79    v = max(eps, xp.sum(spectrum[rank:]) / (n_features - rank))
80    pv = -log(v) * n_samples * (n_features - rank) / 2.0
81
82    m = n_features * rank - rank * (rank + 1.0) / 2.0
83    pp = log(2.0 * xp.pi) * (m + rank) / 2.0
84
85    pa = 0.0
86    spectrum_ = xp.asarray(spectrum, copy=True)
87    spectrum_[rank:n_features] = v
88    for i in range(rank):
89        for j in range(i + 1, spectrum.shape[0]):
90            pa += log(
91                (spectrum[i] - spectrum[j]) * (1.0 / spectrum_[j] - 1.0 / spectrum_[i])
92            ) + log(n_samples)
93
94    ll = pu + pl + pv + pp - pa / 2.0 - rank * log(n_samples) / 2.0
95
96    return ll
97
98
99def _infer_dimension(spectrum, n_samples):
100    """Infers the dimension of a dataset with a given spectrum.
101
102    The returned value will be in [1, n_features - 1].
103    """
104    xp, _ = get_namespace(spectrum)
105
106    ll = xp.empty_like(spectrum)
107    ll[0] = -xp.inf  # we don't want to return n_components = 0
108    for rank in range(1, spectrum.shape[0]):
109        ll[rank] = _assess_dimension(spectrum, rank, n_samples)
110    return xp.argmax(ll)
111
112
113class PCA(_BasePCA):
114    """Principal component analysis (PCA).
115
116    Linear dimensionality reduction using Singular Value Decomposition of the
117    data to project it to a lower dimensional space. The input data is centered
118    but not scaled for each feature before applying the SVD.
119
120    It uses the LAPACK implementation of the full SVD or a randomized truncated
121    SVD by the method of Halko et al. 2009, depending on the shape of the input
122    data and the number of components to extract.
123
124    With sparse inputs, the ARPACK implementation of the truncated SVD can be
125    used (i.e. through :func:`scipy.sparse.linalg.svds`). Alternatively, one
126    may consider :class:`TruncatedSVD` where the data are not centered.
127
128    Notice that this class only supports sparse inputs for some solvers such as
129    "arpack" and "covariance_eigh". See :class:`TruncatedSVD` for an
130    alternative with sparse data.
131
132    For a usage example, see
133    :ref:`sphx_glr_auto_examples_decomposition_plot_pca_iris.py`
134
135    Read more in the :ref:`User Guide <PCA>`.
136
137    Parameters
138    ----------
139    n_components : int, float or 'mle', default=None
140        Number of components to keep.
141        if n_components is not set all components are kept::
142
143            n_components == min(n_samples, n_features)
144
145        If ``n_components == 'mle'`` and ``svd_solver == 'full'``, Minka's
146        MLE is used to guess the dimension. Use of ``n_components == 'mle'``
147        will interpret ``svd_solver == 'auto'`` as ``svd_solver == 'full'``.
148
149        If ``0 < n_components < 1`` and ``svd_solver == 'full'``, select the
150        number of components such that the amount of variance that needs to be
151        explained is greater than the percentage specified by n_components.
152
153        If ``svd_solver == 'arpack'``, the number of components must be
154        strictly less than the minimum of n_features and n_samples.
155
156        Hence, the None case results in::
157
158            n_components == min(n_samples, n_features) - 1
159
160    copy : bool, default=True
161        If False, data passed to fit are overwritten and running
162        fit(X).transform(X) will not yield the expected results,
163        use fit_transform(X) instead.
164
165    whiten : bool, default=False
166        When True (False by default) the `components_` vectors are multiplied
167        by the square root of n_samples and then divided by the singular values
168        to ensure uncorrelated outputs with unit component-wise variances.
169
170        Whitening will remove some information from the transformed signal
171        (the relative variance scales of the components) but can sometime
172        improve the predictive accuracy of the downstream estimators by
173        making their data respect some hard-wired assumptions.
174
175    svd_solver : {'auto', 'full', 'covariance_eigh', 'arpack', 'randomized'},\
176            default='auto'
177        "auto" :
178            The solver is selected by a default 'auto' policy is based on `X.shape` and
179            `n_components`: if the input data has fewer than 1000 features and
180            more than 10 times as many samples, then the "covariance_eigh"
181            solver is used. Otherwise, if the input data is larger than 500x500
182            and the number of components to extract is lower than 80% of the
183            smallest dimension of the data, then the more efficient
184            "randomized" method is selected. Otherwise the exact "full" SVD is
185            computed and optionally truncated afterwards.
186        "full" :
187            Run exact full SVD calling the standard LAPACK solver via
188            `scipy.linalg.svd` and select the components by postprocessing
189        "covariance_eigh" :
190            Precompute the covariance matrix (on centered data), run a
191            classical eigenvalue decomposition on the covariance matrix
192            typically using LAPACK and select the components by postprocessing.
193            This solver is very efficient for n_samples >> n_features and small
194            n_features. It is, however, not tractable otherwise for large
195            n_features (large memory footprint required to materialize the
196            covariance matrix). Also note that compared to the "full" solver,
197            this solver effectively doubles the condition number and is
198            therefore less numerical stable (e.g. on input data with a large
199            range of singular values).
200        "arpack" :
201            Run SVD truncated to `n_components` calling ARPACK solver via
202            `scipy.sparse.linalg.svds`. It requires strictly
203            `0 < n_components < min(X.shape)`
204        "randomized" :
205            Run randomized SVD by the method of Halko et al.
206
207        .. versionadded:: 0.18.0
208
209        .. versionchanged:: 1.5
210            Added the 'covariance_eigh' solver.
211
212    tol : float, default=0.0
213        Tolerance for singular values computed by svd_solver == 'arpack'.
214        Must be of range [0.0, infinity).
215
216        .. versionadded:: 0.18.0
217
218    iterated_power : int or 'auto', default='auto'
219        Number of iterations for the power method computed by
220        svd_solver == 'randomized'.
221        Must be of range [0, infinity).
222
223        .. versionadded:: 0.18.0
224
225    n_oversamples : int, default=10
226        This parameter is only relevant when `svd_solver="randomized"`.
227        It corresponds to the additional number of random vectors to sample the
228        range of `X` so as to ensure proper conditioning. See
229        :func:`~sklearn.utils.extmath.randomized_svd` for more details.
230
231        .. versionadded:: 1.1
232
233    power_iteration_normalizer : {'auto', 'QR', 'LU', 'none'}, default='auto'
234        Power iteration normalizer for randomized SVD solver.
235        Not used by ARPACK. See :func:`~sklearn.utils.extmath.randomized_svd`
236        for more details.
237
238        .. versionadded:: 1.1
239
240    random_state : int, RandomState instance or None, default=None
241        Used when the 'arpack' or 'randomized' solvers are used. Pass an int
242        for reproducible results across multiple function calls.
243        See :term:`Glossary <random_state>`.
244
245        .. versionadded:: 0.18.0
246
247    Attributes
248    ----------
249    components_ : ndarray of shape (n_components, n_features)
250        Principal axes in feature space, representing the directions of
251        maximum variance in the data. Equivalently, the right singular
252        vectors of the centered input data, parallel to its eigenvectors.
253        The components are sorted by decreasing ``explained_variance_``.
254
255    explained_variance_ : ndarray of shape (n_components,)
256        The amount of variance explained by each of the selected components.
257        The variance estimation uses `n_samples - 1` degrees of freedom.
258
259        Equal to n_components largest eigenvalues
260        of the covariance matrix of X.
261
262        .. versionadded:: 0.18
263
264    explained_variance_ratio_ : ndarray of shape (n_components,)
265        Percentage of variance explained by each of the selected components.
266
267        If ``n_components`` is not set then all components are stored and the
268        sum of the ratios is equal to 1.0.
269
270    singular_values_ : ndarray of shape (n_components,)
271        The singular values corresponding to each of the selected components.
272        The singular values are equal to the 2-norms of the ``n_components``
273        variables in the lower-dimensional space.
274
275        .. versionadded:: 0.19
276
277    mean_ : ndarray of shape (n_features,)
278        Per-feature empirical mean, estimated from the training set.
279
280        Equal to `X.mean(axis=0)`.
281
282    n_components_ : int
283        The estimated number of components. When n_components is set
284        to 'mle' or a number between 0 and 1 (with svd_solver == 'full') this
285        number is estimated from input data. Otherwise it equals the parameter
286        n_components, or the lesser value of n_features and n_samples
287        if n_components is None.
288
289    n_samples_ : int
290        Number of samples in the training data.
291
292    noise_variance_ : float
293        The estimated noise covariance following the Probabilistic PCA model
294        from Tipping and Bishop 1999. See "Pattern Recognition and
295        Machine Learning" by C. Bishop, 12.2.1 p. 574 or
296        http://www.miketipping.com/papers/met-mppca.pdf. It is required to
297        compute the estimated data covariance and score samples.
298
299        Equal to the average of (min(n_features, n_samples) - n_components)
300        smallest eigenvalues of the covariance matrix of X.
301
302    n_features_in_ : int
303        Number of features seen during :term:`fit`.
304
305        .. versionadded:: 0.24
306
307    feature_names_in_ : ndarray of shape (`n_features_in_`,)
308        Names of features seen during :term:`fit`. Defined only when `X`
309        has feature names that are all strings.
310
311        .. versionadded:: 1.0
312
313    See Also
314    --------
315    KernelPCA : Kernel Principal Component Analysis.
316    SparsePCA : Sparse Principal Component Analysis.
317    TruncatedSVD : Dimensionality reduction using truncated SVD.
318    IncrementalPCA : Incremental Principal Component Analysis.
319
320    References
321    ----------
322    For n_components == 'mle', this class uses the method from:
323    `Minka, T. P.. "Automatic choice of dimensionality for PCA".
324    In NIPS, pp. 598-604 <https://tminka.github.io/papers/pca/minka-pca.pdf>`_
325
326    Implements the probabilistic PCA model from:
327    `Tipping, M. E., and Bishop, C. M. (1999). "Probabilistic principal
328    component analysis". Journal of the Royal Statistical Society:
329    Series B (Statistical Methodology), 61(3), 611-622.
330    <http://www.miketipping.com/papers/met-mppca.pdf>`_
331    via the score and score_samples methods.
332
333    For svd_solver == 'arpack', refer to `scipy.sparse.linalg.svds`.
334
335    For svd_solver == 'randomized', see:
336    :doi:`Halko, N., Martinsson, P. G., and Tropp, J. A. (2011).
337    "Finding structure with randomness: Probabilistic algorithms for
338    constructing approximate matrix decompositions".
339    SIAM review, 53(2), 217-288.
340    <10.1137/090771806>`
341    and also
342    :doi:`Martinsson, P. G., Rokhlin, V., and Tygert, M. (2011).
343    "A randomized algorithm for the decomposition of matrices".
344    Applied and Computational Harmonic Analysis, 30(1), 47-68.
345    <10.1016/j.acha.2010.02.003>`
346
347    Examples
348    --------
349    >>> import numpy as np
350    >>> from sklearn.decomposition import PCA
351    >>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
352    >>> pca = PCA(n_components=2)
353    >>> pca.fit(X)
354    PCA(n_components=2)
355    >>> print(pca.explained_variance_ratio_)
356    [0.9924 0.0075]
357    >>> print(pca.singular_values_)
358    [6.30061 0.54980]
359
360    >>> pca = PCA(n_components=2, svd_solver='full')
361    >>> pca.fit(X)
362    PCA(n_components=2, svd_solver='full')
363    >>> print(pca.explained_variance_ratio_)
364    [0.9924 0.00755]
365    >>> print(pca.singular_values_)
366    [6.30061 0.54980]
367
368    >>> pca = PCA(n_components=1, svd_solver='arpack')
369    >>> pca.fit(X)
370    PCA(n_components=1, svd_solver='arpack')
371    >>> print(pca.explained_variance_ratio_)
372    [0.99244]
373    >>> print(pca.singular_values_)
374    [6.30061]
375    """
376
377    _parameter_constraints: dict = {
378        "n_components": [
379            Interval(Integral, 0, None, closed="left"),
380            Interval(RealNotInt, 0, 1, closed="neither"),
381            StrOptions({"mle"}),
382            None,
383        ],
384        "copy": ["boolean"],
385        "whiten": ["boolean"],
386        "svd_solver": [
387            StrOptions({"auto", "full", "covariance_eigh", "arpack", "randomized"})
388        ],
389        "tol": [Interval(Real, 0, None, closed="left")],
390        "iterated_power": [
391            StrOptions({"auto"}),
392            Interval(Integral, 0, None, closed="left"),
393        ],
394        "n_oversamples": [Interval(Integral, 1, None, closed="left")],
395        "power_iteration_normalizer": [StrOptions({"auto", "QR", "LU", "none"})],
396        "random_state": ["random_state"],
397    }
398
399    def __init__(
400        self,
401        n_components=None,
402        *,
403        copy=True,
404        whiten=False,
405        svd_solver="auto",
406        tol=0.0,
407        iterated_power="auto",
408        n_oversamples=10,
409        power_iteration_normalizer="auto",
410        random_state=None,
411    ):
412        self.n_components = n_components
413        self.copy = copy
414        self.whiten = whiten
415        self.svd_solver = svd_solver
416        self.tol = tol
417        self.iterated_power = iterated_power
418        self.n_oversamples = n_oversamples
419        self.power_iteration_normalizer = power_iteration_normalizer
420        self.random_state = random_state
421
422    @_fit_context(prefer_skip_nested_validation=True)
423    def fit(self, X, y=None):
424        """Fit the model with X.
425
426        Parameters
427        ----------
428        X : {array-like, sparse matrix} of shape (n_samples, n_features)
429            Training data, where `n_samples` is the number of samples
430            and `n_features` is the number of features.
431
432        y : Ignored
433            Ignored.
434
435        Returns
436        -------
437        self : object
438            Returns the instance itself.
439        """
440        self._fit(X)
441        return self
442
443    @_fit_context(prefer_skip_nested_validation=True)
444    def fit_transform(self, X, y=None):
445        """Fit the model with X and apply the dimensionality reduction on X.
446
447        Parameters
448        ----------
449        X : {array-like, sparse matrix} of shape (n_samples, n_features)
450            Training data, where `n_samples` is the number of samples
451            and `n_features` is the number of features.
452
453        y : Ignored
454            Ignored.
455
456        Returns
457        -------
458        X_new : ndarray of shape (n_samples, n_components)
459            Transformed values.
460
461        Notes
462        -----
463        This method returns a Fortran-ordered array. To convert it to a
464        C-ordered array, use 'np.ascontiguousarray'.
465        """
466        U, S, _, X, x_is_centered, xp = self._fit(X)
467        if U is not None:
468            U = U[:, : self.n_components_]
469
470            if self.whiten:
471                # X_new = X * V / S * sqrt(n_samples) = U * sqrt(n_samples)
472                U *= sqrt(X.shape[0] - 1)
473            else:
474                # X_new = X * V = U * S * Vt * V = U * S
475                U *= S[: self.n_components_]
476
477            return U
478        else:  # solver="covariance_eigh" does not compute U at fit time.
479            return self._transform(X, xp, x_is_centered=x_is_centered)
480
481    def _fit(self, X):
482        """Dispatch to the right submethod depending on the chosen solver."""
483        xp, is_array_api_compliant = get_namespace(X)
484
485        # Raise an error for sparse input and unsupported svd_solver
486        if issparse(X) and self.svd_solver not in ["auto", "arpack", "covariance_eigh"]:
487            raise TypeError(
488                'PCA only support sparse inputs with the "arpack" and'
489                f' "covariance_eigh" solvers, while "{self.svd_solver}" was passed. See'
490                " TruncatedSVD for a possible alternative."
491            )
492        if self.svd_solver == "arpack" and is_array_api_compliant:
493            raise ValueError(
494                "PCA with svd_solver='arpack' is not supported for Array API inputs."
495            )
496
497        # Validate the data, without ever forcing a copy as any solver that
498        # supports sparse input data and the `covariance_eigh` solver are
499        # written in a way to avoid the need for any inplace modification of
500        # the input data contrary to the other solvers.
501        # The copy will happen
502        # later, only if needed, once the solver negotiation below is done.
503        X = validate_data(
504            self,
505            X,
506            dtype=[xp.float64, xp.float32],
507            force_writeable=True,
508            accept_sparse=("csr", "csc"),
509            ensure_2d=True,
510            copy=False,
511        )
512        self._fit_svd_solver = self.svd_solver
513        if self._fit_svd_solver == "auto" and issparse(X):
514            self._fit_svd_solver = "arpack"
515
516        if self.n_components is None:
517            if self._fit_svd_solver != "arpack":
518                n_components = min(X.shape)
519            else:
520                n_components = min(X.shape) - 1
521        else:
522            n_components = self.n_components
523
524        if self._fit_svd_solver == "auto":
525            # Tall and skinny problems are best handled by precomputing the
526            # covariance matrix.
527            if X.shape[1] <= 1_000 and X.shape[0] >= 10 * X.shape[1]:
528                self._fit_svd_solver = "covariance_eigh"
529            # Small problem or n_components == 'mle', just call full PCA
530            elif max(X.shape) <= 500 or n_components == "mle":
531                self._fit_svd_solver = "full"
532            elif 1 <= n_components < 0.8 * min(X.shape):
533                self._fit_svd_solver = "randomized"
534            # This is also the case of n_components in (0, 1)
535            else:
536                self._fit_svd_solver = "full"
537
538        # Call different fits for either full or truncated SVD
539        if self._fit_svd_solver in ("full", "covariance_eigh"):
540            return self._fit_full(X, n_components, xp, is_array_api_compliant)
541        elif self._fit_svd_solver in ["arpack", "randomized"]:
542            return self._fit_truncated(X, n_components, xp)
543
544    def _fit_full(self, X, n_components, xp, is_array_api_compliant):
545        """Fit the model by computing full SVD on X."""
546        n_samples, n_features = X.shape
547
548        if n_components == "mle":
549            if n_samples < n_features:
550                raise ValueError(
551                    "n_components='mle' is only supported if n_samples >= n_features"
552                )
553        elif not 0 <= n_components <= min(n_samples, n_features):
554            raise ValueError(
555                f"n_components={n_components} must be between 0 and "
556                f"min(n_samples, n_features)={min(n_samples, n_features)} with "
557                f"svd_solver={self._fit_svd_solver!r}"
558            )
559
560        self.mean_ = xp.mean(X, axis=0)
561        # When X is a scipy sparse matrix, self.mean_ is a numpy matrix, so we need
562        # to transform it to a 1D array. Note that this is not the case when X
563        # is a scipy sparse array.
564        # TODO: remove the following two lines when scikit-learn only depends
565        # on scipy versions that no longer support scipy.sparse matrices.
566        self.mean_ = xp.reshape(xp.asarray(self.mean_), (-1,))
567
568        if self._fit_svd_solver == "full":
569            X_centered = xp.asarray(X, copy=True) if self.copy else X
570            X_centered -= self.mean_
571            x_is_centered = not self.copy
572
573            if not is_array_api_compliant:
574                # Use scipy.linalg with NumPy/SciPy inputs for the sake of not
575                # introducing unanticipated behavior changes. In the long run we
576                # could instead decide to always use xp.linalg.svd for all inputs,
577                # but that would make this code rely on numpy's SVD instead of
578                # scipy's. It's not 100% clear whether they use the same LAPACK
579                # solver by default though (assuming both are built against the
580                # same BLAS).
581                U, S, Vt = linalg.svd(X_centered, full_matrices=False)
582            else:
583                U, S, Vt = xp.linalg.svd(X_centered, full_matrices=False)
584            explained_variance_ = (S**2) / (n_samples - 1)
585
586        else:
587            assert self._fit_svd_solver == "covariance_eigh"
588            # In the following, we center the covariance matrix C afterwards
589            # (without centering the data X first) to avoid an unnecessary copy
590            # of X. Note that the mean_ attribute is still needed to center
591            # test data in the transform method.
592            #
593            # Note: at the time of writing, `xp.cov` does not exist in the
594            # Array API standard:
595            # https://github.com/data-apis/array-api/issues/43
596            #
597            # Besides, using `numpy.cov`, as of numpy 1.26.0, would not be
598            # memory efficient for our use case when `n_samples >> n_features`:
599            # `numpy.cov` centers a copy of the data before computing the
600            # matrix product instead of subtracting a small `(n_features,
601            # n_features)` square matrix from the gram matrix X.T @ X, as we do
602            # below.
603            x_is_centered = False
604            C = X.T @ X
605            C -= (
606                n_samples
607                * xp.reshape(self.mean_, (-1, 1))
608                * xp.reshape(self.mean_, (1, -1))
609            )
610            C /= n_samples - 1
611            eigenvals, eigenvecs = xp.linalg.eigh(C)
612
613            # When X is a scipy sparse matrix, the following two datastructures
614            # are returned as instances of the soft-deprecated numpy.matrix
615            # class. Note that this problem does not occur when X is a scipy
616            # sparse array (or another other kind of supported array).
617            # TODO: remove the following two lines when scikit-learn only
618            # depends on scipy versions that no longer support scipy.sparse
619            # matrices.
620            eigenvals = xp.reshape(xp.asarray(eigenvals), (-1,))
621            eigenvecs = xp.asarray(eigenvecs)
622
623            eigenvals = xp.flip(eigenvals, axis=0)
624            eigenvecs = xp.flip(eigenvecs, axis=1)
625
626            # The covariance matrix C is positive semi-definite by
627            # construction. However, the eigenvalues returned by xp.linalg.eigh
628            # can be slightly negative due to numerical errors. This would be
629            # an issue for the subsequent sqrt, hence the manual clipping.
630            eigenvals[eigenvals < 0.0] = 0.0
631            explained_variance_ = eigenvals
632
633            # Re-construct SVD of centered X indirectly and make it consistent
634            # with the other solvers.
635            S = xp.sqrt(eigenvals * (n_samples - 1))
636            Vt = eigenvecs.T
637            U = None
638
639        # flip eigenvectors' sign to enforce deterministic output
640        U, Vt = svd_flip(U, Vt, u_based_decision=False)
641
642        components_ = Vt
643
644        # Get variance explained by singular values
645        total_var = xp.sum(explained_variance_)
646        explained_variance_ratio_ = explained_variance_ / total_var
647        singular_values_ = xp.asarray(S, copy=True)  # Store the singular values.
648
649        # Postprocess the number of components required
650        if n_components == "mle":
651            n_components = _infer_dimension(explained_variance_, n_samples)
652        elif 0 < n_components < 1.0:
653            # number of components for which the cumulated explained
654            # variance percentage is superior to the desired threshold
655            # side='right' ensures that number of features selected
656            # their variance is always greater than n_components float
657            # passed. More discussion in issue: #15669
658            if is_array_api_compliant:
659                # Convert to numpy as xp.cumsum and xp.searchsorted are not
660                # part of the Array API standard yet:
661                #
662                # https://github.com/data-apis/array-api/issues/597
663                # https://github.com/data-apis/array-api/issues/688
664                #
665                # Furthermore, it's not always safe to call them for namespaces
666                # that already implement them: for instance as
667                # cupy.searchsorted does not accept a float as second argument.
668                explained_variance_ratio_np = _convert_to_numpy(
669                    explained_variance_ratio_, xp=xp
670                )
671            else:
672                explained_variance_ratio_np = explained_variance_ratio_
673            ratio_cumsum = stable_cumsum(explained_variance_ratio_np)
674            n_components = np.searchsorted(ratio_cumsum, n_components, side="right") + 1
675
676        # Compute noise covariance using Probabilistic PCA model
677        # The sigma2 maximum likelihood (cf. eq. 12.46)
678        if n_components < min(n_features, n_samples):
679            self.noise_variance_ = xp.mean(explained_variance_[n_components:])
680        else:
681            self.noise_variance_ = 0.0
682
683        self.n_samples_ = n_samples
684        self.n_components_ = n_components
685        # Assign a copy of the result of the truncation of the components in
686        # order to:
687        # - release the memory used by the discarded components,
688        # - ensure that the kept components are allocated contiguously in
689        #   memory to make the transform method faster by leveraging cache
690        #   locality.
691        self.components_ = xp.asarray(components_[:n_components, :], copy=True)
692
693        # We do the same for the other arrays for the sake of consistency.
694        self.explained_variance_ = xp.asarray(
695            explained_variance_[:n_components], copy=True
696        )
697        self.explained_variance_ratio_ = xp.asarray(
698            explained_variance_ratio_[:n_components], copy=True
699        )
700        self.singular_values_ = xp.asarray(singular_values_[:n_components], copy=True)
701
702        return U, S, Vt, X, x_is_centered, xp
703
704    def _fit_truncated(self, X, n_components, xp):
705        """Fit the model by computing truncated SVD (by ARPACK or randomized)
706        on X.
707        """
708        n_samples, n_features = X.shape
709
710        svd_solver = self._fit_svd_solver
711        if isinstance(n_components, str):
712            raise ValueError(
713                "n_components=%r cannot be a string with svd_solver='%s'"
714                % (n_components, svd_solver)
715            )
716        elif not 1 <= n_components <= min(n_samples, n_features):
717            raise ValueError(
718                "n_components=%r must be between 1 and "
719                "min(n_samples, n_features)=%r with "
720                "svd_solver='%s'"
721                % (n_components, min(n_samples, n_features), svd_solver)
722            )
723        elif svd_solver == "arpack" and n_components == min(n_samples, n_features):
724            raise ValueError(
725                "n_components=%r must be strictly less than "
726                "min(n_samples, n_features)=%r with "
727                "svd_solver='%s'"
728                % (n_components, min(n_samples, n_features), svd_solver)
729            )
730
731        random_state = check_random_state(self.random_state)
732
733        # Center data
734        total_var = None
735        if issparse(X):
736            self.mean_, var = mean_variance_axis(X, axis=0)
737            total_var = var.sum() * n_samples / (n_samples - 1)  # ddof=1
738            X_centered = _implicit_column_offset(X, self.mean_)
739            x_is_centered = False
740        else:
741            self.mean_ = xp.mean(X, axis=0)
742            X_centered = xp.asarray(X, copy=True) if self.copy else X
743            X_centered -= self.mean_
744            x_is_centered = not self.copy
745
746        if svd_solver == "arpack":
747            v0 = _init_arpack_v0(min(X.shape), random_state)
748            U, S, Vt = svds(X_centered, k=n_components, tol=self.tol, v0=v0)
749            # svds doesn't abide by scipy.linalg.svd/randomized_svd
750            # conventions, so reverse its outputs.
751            S = S[::-1]
752            # flip eigenvectors' sign to enforce deterministic output
753            U, Vt = svd_flip(U[:, ::-1], Vt[::-1], u_based_decision=False)
754
755        elif svd_solver == "randomized":
756            # sign flipping is done inside
757            U, S, Vt = _randomized_svd(
758                X_centered,
759                n_components=n_components,
760                n_oversamples=self.n_oversamples,
761                n_iter=self.iterated_power,
762                power_iteration_normalizer=self.power_iteration_normalizer,
763                flip_sign=False,
764                random_state=random_state,
765            )
766            U, Vt = svd_flip(U, Vt, u_based_decision=False)
767
768        self.n_samples_ = n_samples
769        self.components_ = Vt
770        self.n_components_ = n_components
771
772        # Get variance explained by singular values
773        self.explained_variance_ = (S**2) / (n_samples - 1)
774
775        # Workaround in-place variance calculation since at the time numpy
776        # did not have a way to calculate variance in-place.
777        #
778        # TODO: update this code to either:
779        # * Use the array-api variance calculation, unless memory usage suffers
780        # * Update sklearn.utils.extmath._incremental_mean_and_var to support array-api
781        # See: https://github.com/scikit-learn/scikit-learn/pull/18689#discussion_r1335540991
782        if total_var is None:
783            N = X.shape[0] - 1
784            X_centered **= 2
785            total_var = xp.sum(X_centered) / N
786
787        self.explained_variance_ratio_ = self.explained_variance_ / total_var
788        self.singular_values_ = xp.asarray(S, copy=True)  # Store the singular values.
789
790        if self.n_components_ < min(n_features, n_samples):
791            self.noise_variance_ = total_var - xp.sum(self.explained_variance_)
792            self.noise_variance_ /= min(n_features, n_samples) - n_components
793        else:
794            self.noise_variance_ = 0.0
795
796        return U, S, Vt, X, x_is_centered, xp
797
798    def score_samples(self, X):
799        """Return the log-likelihood of each sample.
800
801        See. "Pattern Recognition and Machine Learning"
802        by C. Bishop, 12.2.1 p. 574
803        or http://www.miketipping.com/papers/met-mppca.pdf
804
805        Parameters
806        ----------
807        X : array-like of shape (n_samples, n_features)
808            The data.
809
810        Returns
811        -------
812        ll : ndarray of shape (n_samples,)
813            Log-likelihood of each sample under the current model.
814        """
815        check_is_fitted(self)
816        xp, _ = get_namespace(X)
817        X = validate_data(self, X, dtype=[xp.float64, xp.float32], reset=False)
818        Xr = X - self.mean_
819        n_features = X.shape[1]
820        precision = self.get_precision()
821        log_like = -0.5 * xp.sum(Xr * (Xr @ precision), axis=1)
822        log_like -= 0.5 * (n_features * log(2.0 * np.pi) - fast_logdet(precision))
823        return log_like
824
825    def score(self, X, y=None):
826        """Return the average log-likelihood of all samples.
827
828        See. "Pattern Recognition and Machine Learning"
829        by C. Bishop, 12.2.1 p. 574
830        or http://www.miketipping.com/papers/met-mppca.pdf
831
832        Parameters
833        ----------
834        X : array-like of shape (n_samples, n_features)
835            The data.
836
837        y : Ignored
838            Ignored.
839
840        Returns
841        -------
842        ll : float
843            Average log-likelihood of the samples under the current model.
844        """
845        xp, _ = get_namespace(X)
846        return float(xp.mean(self.score_samples(X)))
847
848    def __sklearn_tags__(self):
849        tags = super().__sklearn_tags__()
850        tags.transformer_tags.preserves_dtype = ["float64", "float32"]
851        tags.array_api_support = True
852        tags.input_tags.sparse = self.svd_solver in (
853            "auto",
854            "arpack",
855            "covariance_eigh",
856        )
857        return tags
858 
Aluode/PerceptionLabPortable · CoolFace