MNT n_features_in_ consistency in decomposition (#18557)
Co-authored-by: Nicolas Hug <contact@nicolas-hug.com>
This commit is contained in:
parent
73732e5a0b
commit
548a4524b4
|
|
@ -12,7 +12,6 @@ import numpy as np
|
|||
from scipy import linalg
|
||||
|
||||
from ..base import BaseEstimator, TransformerMixin
|
||||
from ..utils import check_array
|
||||
from ..utils.validation import check_is_fitted
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
|
@ -124,7 +123,7 @@ class _BasePCA(TransformerMixin, BaseEstimator, metaclass=ABCMeta):
|
|||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = check_array(X)
|
||||
X = self._validate_data(X, dtype=[np.float64, np.float32], reset=False)
|
||||
if self.mean_ is not None:
|
||||
X = X - self.mean_
|
||||
X_transformed = np.dot(X, self.components_.T)
|
||||
|
|
|
|||
|
|
@ -907,7 +907,7 @@ class _BaseSparseCoding(TransformerMixin):
|
|||
def _transform(self, X, dictionary):
|
||||
"""Private method allowing to accomodate both DictionaryLearning and
|
||||
SparseCoder."""
|
||||
X = check_array(X)
|
||||
X = self._validate_data(X, reset=False)
|
||||
|
||||
code = sparse_encode(
|
||||
X, dictionary, algorithm=self.transform_algorithm,
|
||||
|
|
@ -1622,7 +1622,6 @@ class MiniBatchDictionaryLearning(_BaseSparseCoding, BaseEstimator):
|
|||
"""
|
||||
if not hasattr(self, 'random_state_'):
|
||||
self.random_state_ = check_random_state(self.random_state)
|
||||
X = check_array(X)
|
||||
if hasattr(self, 'components_'):
|
||||
dict_init = self.components_
|
||||
else:
|
||||
|
|
@ -1630,6 +1629,7 @@ class MiniBatchDictionaryLearning(_BaseSparseCoding, BaseEstimator):
|
|||
inner_stats = getattr(self, 'inner_stats_', None)
|
||||
if iter_offset is None:
|
||||
iter_offset = getattr(self, 'iter_offset_', 0)
|
||||
X = self._validate_data(X, reset=(iter_offset == 0))
|
||||
U, (A, B) = dict_learning_online(
|
||||
X, self.n_components, alpha=self.alpha,
|
||||
n_iter=1, method=self.fit_algorithm,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from scipy import linalg
|
|||
|
||||
|
||||
from ..base import BaseEstimator, TransformerMixin
|
||||
from ..utils import check_array, check_random_state
|
||||
from ..utils import check_random_state
|
||||
from ..utils.extmath import fast_logdet, randomized_svd, squared_norm
|
||||
from ..utils.validation import check_is_fitted, _deprecate_positional_args
|
||||
from ..exceptions import ConvergenceWarning
|
||||
|
|
@ -279,7 +279,7 @@ class FactorAnalysis(TransformerMixin, BaseEstimator):
|
|||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = check_array(X)
|
||||
X = self._validate_data(X, reset=False)
|
||||
Ih = np.eye(len(self.components_))
|
||||
|
||||
X_transformed = X - self.mean_
|
||||
|
|
@ -350,7 +350,7 @@ class FactorAnalysis(TransformerMixin, BaseEstimator):
|
|||
Log-likelihood of each sample under the current model
|
||||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = self._validate_data(X, reset=False)
|
||||
Xr = X - self.mean_
|
||||
precision = self.get_precision()
|
||||
n_features = X.shape[1]
|
||||
|
|
|
|||
|
|
@ -584,7 +584,7 @@ class FastICA(TransformerMixin, BaseEstimator):
|
|||
and n_features is the number of features.
|
||||
|
||||
copy : bool, default=True
|
||||
If False, data passed to fit are overwritten. Defaults to True.
|
||||
If False, data passed to fit can be overwritten. Defaults to True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -592,7 +592,8 @@ class FastICA(TransformerMixin, BaseEstimator):
|
|||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = check_array(X, copy=copy, dtype=FLOAT_DTYPES)
|
||||
X = self._validate_data(X, copy=(copy and self.whiten),
|
||||
dtype=FLOAT_DTYPES, reset=False)
|
||||
if self.whiten:
|
||||
X -= self.mean_
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import numpy as np
|
|||
from scipy import linalg, sparse
|
||||
|
||||
from ._base import _BasePCA
|
||||
from ..utils import check_array, gen_batches
|
||||
from ..utils import gen_batches
|
||||
from ..utils.extmath import svd_flip, _incremental_mean_and_var
|
||||
from ..utils.validation import _deprecate_positional_args
|
||||
|
||||
|
|
@ -234,15 +234,18 @@ class IncrementalPCA(_BasePCA):
|
|||
self : object
|
||||
Returns the instance itself.
|
||||
"""
|
||||
first_pass = not hasattr(self, "components_")
|
||||
if check_input:
|
||||
if sparse.issparse(X):
|
||||
raise TypeError(
|
||||
"IncrementalPCA.partial_fit does not support "
|
||||
"sparse input. Either convert data to dense "
|
||||
"or use IncrementalPCA.fit to do so in batches.")
|
||||
X = check_array(X, copy=self.copy, dtype=[np.float64, np.float32])
|
||||
X = self._validate_data(
|
||||
X, copy=self.copy, dtype=[np.float64, np.float32],
|
||||
reset=first_pass)
|
||||
n_samples, n_features = X.shape
|
||||
if not hasattr(self, 'components_'):
|
||||
if first_pass:
|
||||
self.components_ = None
|
||||
|
||||
if self.n_components is None:
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@ class KernelPCA(TransformerMixin, BaseEstimator):
|
|||
X_new : ndarray of shape (n_samples, n_components)
|
||||
"""
|
||||
check_is_fitted(self)
|
||||
X = self._validate_data(X, accept_sparse='csr', reset=False)
|
||||
|
||||
# Compute centered gram matrix between X and training data X_fit_
|
||||
K = self._centerer.transform(self._get_kernel(X, self.X_fit_))
|
||||
|
|
|
|||
|
|
@ -509,17 +509,9 @@ class LatentDirichletAllocation(TransformerMixin, BaseEstimator):
|
|||
"""
|
||||
self._check_params()
|
||||
first_time = not hasattr(self, 'components_')
|
||||
|
||||
# In theory reset should be equal to `first_time`, but there are tests
|
||||
# checking the input number of feature and they expect a specific
|
||||
# string, which is not the same one raised by check_n_features. So we
|
||||
# don't check n_features_in_ here for now (it's done with adhoc code in
|
||||
# the estimator anyway).
|
||||
# TODO: set reset=first_time when addressing reset in
|
||||
# predict/transform/etc.
|
||||
reset_n_features = True
|
||||
X = self._check_non_neg_array(X, reset_n_features,
|
||||
"LatentDirichletAllocation.partial_fit")
|
||||
X = self._check_non_neg_array(
|
||||
X, reset_n_features=first_time,
|
||||
whom="LatentDirichletAllocation.partial_fit")
|
||||
n_samples, n_features = X.shape
|
||||
batch_size = self.batch_size
|
||||
|
||||
|
|
@ -663,6 +655,10 @@ class LatentDirichletAllocation(TransformerMixin, BaseEstimator):
|
|||
doc_topic_distr : ndarray of shape (n_samples, n_components)
|
||||
Document topic distribution for X.
|
||||
"""
|
||||
check_is_fitted(self)
|
||||
X = self._check_non_neg_array(
|
||||
X, reset_n_features=False,
|
||||
whom="LatentDirichletAllocation.transform")
|
||||
doc_topic_distr = self._unnormalized_transform(X)
|
||||
doc_topic_distr /= doc_topic_distr.sum(axis=1)[:, np.newaxis]
|
||||
return doc_topic_distr
|
||||
|
|
@ -758,7 +754,8 @@ class LatentDirichletAllocation(TransformerMixin, BaseEstimator):
|
|||
score : float
|
||||
Use approximate bound as score.
|
||||
"""
|
||||
X = self._check_non_neg_array(X, reset_n_features=True,
|
||||
check_is_fitted(self)
|
||||
X = self._check_non_neg_array(X, reset_n_features=False,
|
||||
whom="LatentDirichletAllocation.score")
|
||||
|
||||
doc_topic_distr = self._unnormalized_transform(X)
|
||||
|
|
|
|||
|
|
@ -1299,6 +1299,8 @@ class NMF(TransformerMixin, BaseEstimator):
|
|||
X = self._validate_data(X, accept_sparse=('csr', 'csc'),
|
||||
dtype=[np.float64, np.float32])
|
||||
|
||||
# XXX: input data validation is performed again in
|
||||
# non_negative_factorization.
|
||||
W, H, n_iter_ = non_negative_factorization(
|
||||
X=X, W=W, H=H, n_components=self.n_components, init=self.init,
|
||||
update_H=True, solver=self.solver, beta_loss=self.beta_loss,
|
||||
|
|
@ -1347,7 +1349,12 @@ class NMF(TransformerMixin, BaseEstimator):
|
|||
Transformed data.
|
||||
"""
|
||||
check_is_fitted(self)
|
||||
X = self._validate_data(X, accept_sparse=('csr', 'csc'),
|
||||
dtype=[np.float64, np.float32],
|
||||
reset=False)
|
||||
|
||||
# XXX: input data validation is performed again in
|
||||
# non_negative_factorization.
|
||||
W, _, n_iter_ = non_negative_factorization(
|
||||
X=X, W=None, H=self.components_, n_components=self.n_components_,
|
||||
init=self.init, update_H=False, solver=self.solver,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from scipy.sparse.linalg import svds
|
|||
|
||||
from ._base import _BasePCA
|
||||
from ..utils import check_random_state
|
||||
from ..utils import check_array
|
||||
from ..utils.extmath import fast_logdet, randomized_svd, svd_flip
|
||||
from ..utils.extmath import stable_cumsum
|
||||
from ..utils.validation import check_is_fitted
|
||||
|
|
@ -583,7 +582,7 @@ class PCA(_BasePCA):
|
|||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = check_array(X)
|
||||
X = self._validate_data(X, dtype=[np.float64, np.float32], reset=False)
|
||||
Xr = X - self.mean_
|
||||
n_features = X.shape[1]
|
||||
precision = self.get_precision()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import numpy as np
|
||||
|
||||
from ..utils import check_random_state, check_array
|
||||
from ..utils import check_random_state
|
||||
from ..utils.validation import check_is_fitted
|
||||
from ..utils.validation import _deprecate_positional_args
|
||||
from ..linear_model import ridge_regression
|
||||
|
|
@ -197,7 +197,7 @@ class SparsePCA(TransformerMixin, BaseEstimator):
|
|||
"""
|
||||
check_is_fitted(self)
|
||||
|
||||
X = check_array(X)
|
||||
X = self._validate_data(X, reset=False)
|
||||
X = X - self.mean_
|
||||
|
||||
U = ridge_regression(self.components_.T, X.T, self.ridge_alpha,
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ class TruncatedSVD(TransformerMixin, BaseEstimator):
|
|||
X_new : ndarray of shape (n_samples, n_components)
|
||||
Reduced version of X. This will always be a dense array.
|
||||
"""
|
||||
X = check_array(X, accept_sparse=['csr', 'csc'])
|
||||
check_is_fitted(self)
|
||||
X = self._validate_data(X, accept_sparse=['csr', 'csc'], reset=False)
|
||||
return safe_sparse_dot(X, self.components_.T)
|
||||
|
||||
def inverse_transform(self, X):
|
||||
|
|
|
|||
|
|
@ -137,21 +137,6 @@ def test_lda_fit_transform(method):
|
|||
assert_array_almost_equal(X_fit, X_trans, 4)
|
||||
|
||||
|
||||
def test_lda_partial_fit_dim_mismatch():
|
||||
# test `n_features` mismatch in `partial_fit`
|
||||
rng = np.random.RandomState(0)
|
||||
n_components = rng.randint(3, 6)
|
||||
n_col = rng.randint(6, 10)
|
||||
X_1 = np.random.randint(4, size=(10, n_col))
|
||||
X_2 = np.random.randint(4, size=(10, n_col + 1))
|
||||
lda = LatentDirichletAllocation(n_components=n_components,
|
||||
learning_offset=5., total_samples=20,
|
||||
random_state=rng)
|
||||
lda.partial_fit(X_1)
|
||||
with pytest.raises(ValueError, match=r"^The provided data has"):
|
||||
lda.partial_fit(X_2)
|
||||
|
||||
|
||||
def test_invalid_params():
|
||||
# test `_check_params` method
|
||||
X = np.ones((5, 10))
|
||||
|
|
@ -190,20 +175,6 @@ def test_lda_no_component_error():
|
|||
lda.perplexity(X)
|
||||
|
||||
|
||||
def test_lda_transform_mismatch():
|
||||
# test `n_features` mismatch in partial_fit and transform
|
||||
rng = np.random.RandomState(0)
|
||||
X = rng.randint(4, size=(20, 10))
|
||||
X_2 = rng.randint(4, size=(10, 8))
|
||||
|
||||
n_components = rng.randint(3, 6)
|
||||
lda = LatentDirichletAllocation(n_components=n_components,
|
||||
random_state=rng)
|
||||
lda.partial_fit(X)
|
||||
with pytest.raises(ValueError, match=r"^The provided data has"):
|
||||
lda.partial_fit(X_2)
|
||||
|
||||
|
||||
@if_safe_multiprocessing_with_blas
|
||||
@pytest.mark.parametrize('method', ('online', 'batch'))
|
||||
def test_lda_multi_jobs(method):
|
||||
|
|
|
|||
|
|
@ -289,7 +289,6 @@ N_FEATURES_IN_AFTER_FIT_MODULES_TO_IGNORE = {
|
|||
'compose',
|
||||
'covariance',
|
||||
'cross_decomposition',
|
||||
'decomposition',
|
||||
'discriminant_analysis',
|
||||
'ensemble',
|
||||
'feature_extraction',
|
||||
|
|
@ -324,4 +323,5 @@ N_FEATURES_IN_AFTER_FIT_ESTIMATORS = [
|
|||
@pytest.mark.parametrize("estimator", N_FEATURES_IN_AFTER_FIT_ESTIMATORS,
|
||||
ids=_get_check_estimator_ids)
|
||||
def test_check_n_features_in_after_fitting(estimator):
|
||||
_set_checking_parameters(estimator)
|
||||
check_n_features_in_after_fitting(estimator.__class__.__name__, estimator)
|
||||
|
|
|
|||
Loading…
Reference in New Issue