MNT n_features_in_ consistency in decomposition (#18557)

Co-authored-by: Nicolas Hug <contact@nicolas-hug.com>
This commit is contained in:
Olivier Grisel 2020-10-08 15:47:24 +02:00 committed by GitHub
parent 73732e5a0b
commit 548a4524b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 37 additions and 59 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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]

View File

@ -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_

View File

@ -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:

View File

@ -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_))

View File

@ -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)

View File

@ -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,

View File

@ -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()

View File

@ -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,

View File

@ -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):

View File

@ -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):

View File

@ -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)