Aluode/PerceptionLabPortable
0
1"""Truncated SVD for sparse matrices, aka latent semantic analysis (LSA)."""
2
3# Authors: The scikit-learn developers
4# SPDX-License-Identifier: BSD-3-Clause
5
6from numbers import Integral, Real
7
8import numpy as np
9import scipy.sparse as sp
10from scipy.sparse.linalg import svds
11
12from ..base import (
13 BaseEstimator,
14 ClassNamePrefixFeaturesOutMixin,
15 TransformerMixin,
16 _fit_context,
17)
18from ..utils import check_array, check_random_state
19from ..utils._arpack import _init_arpack_v0
20from ..utils._param_validation import Interval, StrOptions
21from ..utils.extmath import _randomized_svd, safe_sparse_dot, svd_flip
22from ..utils.sparsefuncs import mean_variance_axis
23from ..utils.validation import check_is_fitted, validate_data
24
25__all__ = ["TruncatedSVD"]
26
27
28class TruncatedSVD(ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstimator):
29 """Dimensionality reduction using truncated SVD (aka LSA).
30
31 This transformer performs linear dimensionality reduction by means of
32 truncated singular value decomposition (SVD). Contrary to PCA, this
33 estimator does not center the data before computing the singular value
34 decomposition. This means it can work with sparse matrices
35 efficiently.
36
37 In particular, truncated SVD works on term count/tf-idf matrices as
38 returned by the vectorizers in :mod:`sklearn.feature_extraction.text`. In
39 that context, it is known as latent semantic analysis (LSA).
40
41 This estimator supports two algorithms: a fast randomized SVD solver, and
42 a "naive" algorithm that uses ARPACK as an eigensolver on `X * X.T` or
43 `X.T * X`, whichever is more efficient.
44
45 Read more in the :ref:`User Guide <LSA>`.
46
47 Parameters
48 ----------
49 n_components : int, default=2
50 Desired dimensionality of output data.
51 If algorithm='arpack', must be strictly less than the number of features.
52 If algorithm='randomized', must be less than or equal to the number of features.
53 The default value is useful for visualisation. For LSA, a value of
54 100 is recommended.
55
56 algorithm : {'arpack', 'randomized'}, default='randomized'
57 SVD solver to use. Either "arpack" for the ARPACK wrapper in SciPy
58 (scipy.sparse.linalg.svds), or "randomized" for the randomized
59 algorithm due to Halko (2009).
60
61 n_iter : int, default=5
62 Number of iterations for randomized SVD solver. Not used by ARPACK. The
63 default is larger than the default in
64 :func:`~sklearn.utils.extmath.randomized_svd` to handle sparse
65 matrices that may have large slowly decaying spectrum.
66
67 n_oversamples : int, default=10
68 Number of oversamples for randomized SVD solver. Not used by ARPACK.
69 See :func:`~sklearn.utils.extmath.randomized_svd` for a complete
70 description.
71
72 .. versionadded:: 1.1
73
74 power_iteration_normalizer : {'auto', 'QR', 'LU', 'none'}, default='auto'
75 Power iteration normalizer for randomized SVD solver.
76 Not used by ARPACK. See :func:`~sklearn.utils.extmath.randomized_svd`
77 for more details.
78
79 .. versionadded:: 1.1
80
81 random_state : int, RandomState instance or None, default=None
82 Used during randomized svd. Pass an int for reproducible results across
83 multiple function calls.
84 See :term:`Glossary <random_state>`.
85
86 tol : float, default=0.0
87 Tolerance for ARPACK. 0 means machine precision. Ignored by randomized
88 SVD solver.
89
90 Attributes
91 ----------
92 components_ : ndarray of shape (n_components, n_features)
93 The right singular vectors of the input data.
94
95 explained_variance_ : ndarray of shape (n_components,)
96 The variance of the training samples transformed by a projection to
97 each component.
98
99 explained_variance_ratio_ : ndarray of shape (n_components,)
100 Percentage of variance explained by each of the selected components.
101
102 singular_values_ : ndarray of shape (n_components,)
103 The singular values corresponding to each of the selected components.
104 The singular values are equal to the 2-norms of the ``n_components``
105 variables in the lower-dimensional space.
106
107 n_features_in_ : int
108 Number of features seen during :term:`fit`.
109
110 .. versionadded:: 0.24
111
112 feature_names_in_ : ndarray of shape (`n_features_in_`,)
113 Names of features seen during :term:`fit`. Defined only when `X`
114 has feature names that are all strings.
115
116 .. versionadded:: 1.0
117
118 See Also
119 --------
120 DictionaryLearning : Find a dictionary that sparsely encodes data.
121 FactorAnalysis : A simple linear generative model with
122 Gaussian latent variables.
123 IncrementalPCA : Incremental principal components analysis.
124 KernelPCA : Kernel Principal component analysis.
125 NMF : Non-Negative Matrix Factorization.
126 PCA : Principal component analysis.
127
128 Notes
129 -----
130 SVD suffers from a problem called "sign indeterminacy", which means the
131 sign of the ``components_`` and the output from transform depend on the
132 algorithm and random state. To work around this, fit instances of this
133 class to data once, then keep the instance around to do transformations.
134
135 References
136 ----------
137 :arxiv:`Halko, et al. (2009). "Finding structure with randomness:
138 Stochastic algorithms for constructing approximate matrix decompositions"
139 <0909.4061>`
140
141 Examples
142 --------
143 >>> from sklearn.decomposition import TruncatedSVD
144 >>> from scipy.sparse import csr_matrix
145 >>> import numpy as np
146 >>> np.random.seed(0)
147 >>> X_dense = np.random.rand(100, 100)
148 >>> X_dense[:, 2 * np.arange(50)] = 0
149 >>> X = csr_matrix(X_dense)
150 >>> svd = TruncatedSVD(n_components=5, n_iter=7, random_state=42)
151 >>> svd.fit(X)
152 TruncatedSVD(n_components=5, n_iter=7, random_state=42)
153 >>> print(svd.explained_variance_ratio_)
154 [0.0157 0.0512 0.0499 0.0479 0.0453]
155 >>> print(svd.explained_variance_ratio_.sum())
156 0.2102
157 >>> print(svd.singular_values_)
158 [35.2410 4.5981 4.5420 4.4486 4.3288]
159 """
160
161 _parameter_constraints: dict = {
162 "n_components": [Interval(Integral, 1, None, closed="left")],
163 "algorithm": [StrOptions({"arpack", "randomized"})],
164 "n_iter": [Interval(Integral, 0, None, closed="left")],
165 "n_oversamples": [Interval(Integral, 1, None, closed="left")],
166 "power_iteration_normalizer": [StrOptions({"auto", "OR", "LU", "none"})],
167 "random_state": ["random_state"],
168 "tol": [Interval(Real, 0, None, closed="left")],
169 }
170
171 def __init__(
172 self,
173 n_components=2,
174 *,
175 algorithm="randomized",
176 n_iter=5,
177 n_oversamples=10,
178 power_iteration_normalizer="auto",
179 random_state=None,
180 tol=0.0,
181 ):
182 self.algorithm = algorithm
183 self.n_components = n_components
184 self.n_iter = n_iter
185 self.n_oversamples = n_oversamples
186 self.power_iteration_normalizer = power_iteration_normalizer
187 self.random_state = random_state
188 self.tol = tol
189
190 def fit(self, X, y=None):
191 """Fit model on training data X.
192
193 Parameters
194 ----------
195 X : {array-like, sparse matrix} of shape (n_samples, n_features)
196 Training data.
197
198 y : Ignored
199 Not used, present here for API consistency by convention.
200
201 Returns
202 -------
203 self : object
204 Returns the transformer object.
205 """
206 self.fit_transform(X)
207 return self
208
209 @_fit_context(prefer_skip_nested_validation=True)
210 def fit_transform(self, X, y=None):
211 """Fit model to X and perform dimensionality reduction on X.
212
213 Parameters
214 ----------
215 X : {array-like, sparse matrix} of shape (n_samples, n_features)
216 Training data.
217
218 y : Ignored
219 Not used, present here for API consistency by convention.
220
221 Returns
222 -------
223 X_new : ndarray of shape (n_samples, n_components)
224 Reduced version of X. This will always be a dense array.
225 """
226 X = validate_data(self, X, accept_sparse=["csr", "csc"], ensure_min_features=2)
227 random_state = check_random_state(self.random_state)
228
229 if self.algorithm == "arpack":
230 v0 = _init_arpack_v0(min(X.shape), random_state)
231 U, Sigma, VT = svds(X, k=self.n_components, tol=self.tol, v0=v0)
232 # svds doesn't abide by scipy.linalg.svd/randomized_svd
233 # conventions, so reverse its outputs.
234 Sigma = Sigma[::-1]
235 # u_based_decision=False is needed to be consistent with PCA.
236 U, VT = svd_flip(U[:, ::-1], VT[::-1], u_based_decision=False)
237
238 elif self.algorithm == "randomized":
239 if self.n_components > X.shape[1]:
240 raise ValueError(
241 f"n_components({self.n_components}) must be <="
242 f" n_features({X.shape[1]})."
243 )
244 U, Sigma, VT = _randomized_svd(
245 X,
246 self.n_components,
247 n_iter=self.n_iter,
248 n_oversamples=self.n_oversamples,
249 power_iteration_normalizer=self.power_iteration_normalizer,
250 random_state=random_state,
251 flip_sign=False,
252 )
253 U, VT = svd_flip(U, VT, u_based_decision=False)
254
255 self.components_ = VT
256
257 # As a result of the SVD approximation error on X ~ U @ Sigma @ V.T,
258 # X @ V is not the same as U @ Sigma
259 if self.algorithm == "randomized" or (
260 self.algorithm == "arpack" and self.tol > 0
261 ):
262 X_transformed = safe_sparse_dot(X, self.components_.T)
263 else:
264 X_transformed = U * Sigma
265
266 # Calculate explained variance & explained variance ratio
267 self.explained_variance_ = exp_var = np.var(X_transformed, axis=0)
268 if sp.issparse(X):
269 _, full_var = mean_variance_axis(X, axis=0)
270 full_var = full_var.sum()
271 else:
272 full_var = np.var(X, axis=0).sum()
273 self.explained_variance_ratio_ = exp_var / full_var
274 self.singular_values_ = Sigma # Store the singular values.
275
276 return X_transformed
277
278 def transform(self, X):
279 """Perform dimensionality reduction on X.
280
281 Parameters
282 ----------
283 X : {array-like, sparse matrix} of shape (n_samples, n_features)
284 New data.
285
286 Returns
287 -------
288 X_new : ndarray of shape (n_samples, n_components)
289 Reduced version of X. This will always be a dense array.
290 """
291 check_is_fitted(self)
292 X = validate_data(self, X, accept_sparse=["csr", "csc"], reset=False)
293 return safe_sparse_dot(X, self.components_.T)
294
295 def inverse_transform(self, X):
296 """Transform X back to its original space.
297
298 Returns an array X_original whose transform would be X.
299
300 Parameters
301 ----------
302 X : array-like of shape (n_samples, n_components)
303 New data.
304
305 Returns
306 -------
307 X_original : ndarray of shape (n_samples, n_features)
308 Note that this is always a dense array.
309 """
310 X = check_array(X)
311 return np.dot(X, self.components_)
312
313 def __sklearn_tags__(self):
314 tags = super().__sklearn_tags__()
315 tags.input_tags.sparse = True
316 tags.transformer_tags.preserves_dtype = ["float64", "float32"]
317 return tags
318
319 @property
320 def _n_features_out(self):
321 """Number of transformed output features."""
322 return self.components_.shape[0]
323 